diff --git a/PLAN.md b/PLAN.md index ef9e296..c1340d2 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,638 +1,638 @@ -# CryptoPulse Portfolio Management Feature - -## Overview - -Add user authentication and portfolio management capabilities to CryptoPulse, allowing users to track their cryptocurrency investments, analyze performance, and make informed investment decisions. - -**Approach:** Build UI/Frontend first with mock data, then integrate database at the end. - ---- - -## Table of Contents - -1. [Feature Summary](#feature-summary) -2. [Implementation Phases](#implementation-phases) -3. [Phase Details](#phase-details) -4. [File Structure](#file-structure) -5. [Database Schema (Phase 5)](#database-schema-phase-5) - ---- - -## Feature Summary - -| Feature | Description | -|---------|-------------| -| **User Registration** | Create account with email/password | -| **User Login** | Secure authentication with sessions | -| **Portfolio Dashboard** | View all holdings, total value, profit/loss | -| **Add Holdings** | Add coins with purchase price, quantity, date | -| **Edit/Delete Holdings** | Modify or remove existing holdings | -| **Portfolio Analytics** | Charts, allocation breakdown, performance metrics | -| **Watchlist** | Track coins without owning them | - ---- - -## Implementation Phases - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ IMPLEMENTATION ROADMAP │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ PHASE 1 PHASE 2 PHASE 3 PHASE 4 PHASE 5│ -│ ──────── ──────── ──────── ──────── ────────│ -│ │ -│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ -│ │ Auth │──────▶│ Port- │──────▶│ Anal- │──────▶│ Watch │──────▶│ DB │ │ -│ │ UI │ │ folio │ │ ytics │ │ list │ │ Integ │ │ -│ │ │ │ UI │ │ UI │ │ UI │ │ │ │ -│ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘ │ -│ │ -│ • Login page • Dashboard • Pie chart • Watchlist • MongoDB │ -│ • Register • Add holding • Line chart • Price alerts • User model│ -│ • Mock auth • Holdings • Metrics • Mock data • Portfolio │ -│ • Sessions • Edit/Delete • Summary • UI complete • Migrate │ -│ • Mock data • Mock data • Go live │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Phase Details - ---- - -## Phase 1: Authentication UI - -**Goal:** Create login/register pages with mock authentication (no database yet) - -### Tasks - -- [ ] Create `/views/auth/login.ejs` - Login page -- [ ] Create `/views/auth/register.ejs` - Registration page -- [ ] Create `/public/css/auth.css` - Auth page styling -- [ ] Create `/routes/auth.js` - Auth routes -- [ ] Implement mock authentication (hardcoded user or localStorage) -- [ ] Set up express-session for session management -- [ ] Update header navigation (show Login/Logout based on session) -- [ ] Create user dropdown menu when logged in - -### New Files -``` -├── routes/auth.js -├── views/auth/ -│ ├── login.ejs -│ └── register.ejs -├── public/css/auth.css -└── public/js/auth.js -``` - -### Login Page Design -``` -┌────────────────────────────────────────────────────────┐ -│ CryptoPulse │ -├────────────────────────────────────────────────────────┤ -│ │ -│ Welcome Back │ -│ │ -│ ┌─────────────────────────┐ │ -│ │ 📧 Email │ │ -│ └─────────────────────────┘ │ -│ │ -│ ┌─────────────────────────┐ │ -│ │ 🔒 Password │ │ -│ └─────────────────────────┘ │ -│ │ -│ ☐ Remember me │ -│ │ -│ ┌─────────────────────────┐ │ -│ │ LOGIN │ │ -│ └─────────────────────────┘ │ -│ │ -│ Don't have an account? Register │ -│ │ -└────────────────────────────────────────────────────────┘ -``` - -### Register Page Design -``` -┌────────────────────────────────────────────────────────┐ -│ CryptoPulse │ -├────────────────────────────────────────────────────────┤ -│ │ -│ Create Account │ -│ │ -│ ┌─────────────────────────┐ │ -│ │ 👤 Full Name │ │ -│ └─────────────────────────┘ │ -│ │ -│ ┌─────────────────────────┐ │ -│ │ 📧 Email │ │ -│ └─────────────────────────┘ │ -│ │ -│ ┌─────────────────────────┐ │ -│ │ 🔒 Password │ │ -│ └─────────────────────────┘ │ -│ │ -│ ┌─────────────────────────┐ │ -│ │ 🔒 Confirm Password │ │ -│ └─────────────────────────┘ │ -│ │ -│ ┌─────────────────────────┐ │ -│ │ REGISTER │ │ -│ └─────────────────────────┘ │ -│ │ -│ Already have an account? Login │ -│ │ -└────────────────────────────────────────────────────────┘ -``` - -### Updated Navigation -``` -(Logged Out) -┌──────────────────────────────────────────────────────────────────┐ -│ CryptoPulse │ Home │ Markets │ News │ About │ Login │ -└──────────────────────────────────────────────────────────────────┘ - -(Logged In) -┌──────────────────────────────────────────────────────────────────┐ -│ CryptoPulse │ Home │ Markets │ Portfolio │ News │ About │ User▼│ -└──────────────────────────────────────────────────────────────────┘ - ├─ Profile - ├─ Watchlist - └─ Logout -``` - -### Dependencies for Phase 1 -```bash -npm install express-session dotenv -``` - -### Mock Authentication Logic -```javascript -// Temporary mock user (will be replaced by DB in Phase 5) -const mockUsers = [ - { - id: 1, - name: 'Demo User', - email: 'demo@cryptopulse.com', - password: 'demo123' // In Phase 5, this will be hashed - } -]; -``` - ---- - -## Phase 2: Portfolio Dashboard UI - -**Goal:** Create portfolio dashboard with add/edit/delete functionality using mock data - -### Tasks - -- [ ] Create `/views/portfolio/dashboard.ejs` - Main portfolio page -- [ ] Create `/public/css/portfolio.css` - Portfolio styling -- [ ] Create `/public/js/portfolio.js` - Portfolio interactions -- [ ] Create `/routes/portfolio.js` - Portfolio routes -- [ ] Build holdings table component -- [ ] Create "Add Holding" modal with coin search -- [ ] Implement edit holding functionality -- [ ] Implement delete holding with confirmation -- [ ] Calculate and display portfolio metrics (mock data) -- [ ] Create authentication middleware (protect portfolio routes) - -### New Files -``` -├── routes/portfolio.js -├── middleware/auth.js -├── views/portfolio/ -│ └── dashboard.ejs -├── public/css/portfolio.css -└── public/js/portfolio.js -``` - -### Portfolio Dashboard Design -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ PORTFOLIO DASHBOARD [+ Add Holding] │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ Total Value │ │ Total Invested │ │ Profit/Loss │ │ -│ │ $45,230.50 │ │ $38,500.00 │ │ +$6,730 (+17%) │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────────────┐ │ -│ │ MY HOLDINGS │ │ -│ ├────────┬────────┬───────────┬───────────┬──────────┬─────────────┤ │ -│ │ Coin │ Amount │ Avg Price │ Current │ P/L │ Actions │ │ -│ ├────────┼────────┼───────────┼───────────┼──────────┼─────────────┤ │ -│ │ ₿ BTC │ 0.5 │ $42,000 │ $67,000 │ +59.5% │ ✏️ 🗑️ │ │ -│ │ ⟠ ETH │ 3.2 │ $2,800 │ $3,500 │ +25.0% │ ✏️ 🗑️ │ │ -│ │ ◎ SOL │ 15 │ $85 │ $145 │ +70.5% │ ✏️ 🗑️ │ │ -│ │ ⬡ BNB │ 2 │ $320 │ $580 │ +81.2% │ ✏️ 🗑️ │ │ -│ └────────┴────────┴───────────┴───────────┴──────────┴─────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### Add Holding Modal Design -``` -┌────────────────────────────────────────────┐ -│ Add New Holding [X] │ -├────────────────────────────────────────────┤ -│ │ -│ Search Coin │ -│ ┌──────────────────────────────────────┐ │ -│ │ 🔍 Search for a coin... │ │ -│ └──────────────────────────────────────┘ │ -│ ┌────────────────────────────────────┐ │ -│ │ ₿ Bitcoin (BTC) │ │ -│ │ ⟠ Ethereum (ETH) │ │ -│ │ ◎ Solana (SOL) │ │ -│ └────────────────────────────────────┘ │ -│ │ -│ Quantity │ -│ ┌──────────────────────────────────────┐ │ -│ │ 0.00 │ │ -│ └──────────────────────────────────────┘ │ -│ │ -│ Purchase Price (per coin) │ -│ ┌──────────────────────────────────────┐ │ -│ │ $ 0.00 [Use Current Price] │ │ -│ └──────────────────────────────────────┘ │ -│ │ -│ Purchase Date │ -│ ┌──────────────────────────────────────┐ │ -│ │ 📅 Select date │ │ -│ └──────────────────────────────────────┘ │ -│ │ -│ Notes (optional) │ -│ ┌──────────────────────────────────────┐ │ -│ │ │ │ -│ └──────────────────────────────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────────┐ │ -│ │ Cancel │ │ Add Holding │ │ -│ └──────────────┘ └──────────────────┘ │ -│ │ -└────────────────────────────────────────────┘ -``` - -### Mock Portfolio Data -```javascript -// Temporary mock data (will be replaced by DB in Phase 5) -const mockPortfolio = { - holdings: [ - { - id: 1, - coinId: 'bitcoin', - symbol: 'BTC', - name: 'Bitcoin', - quantity: 0.5, - purchasePrice: 42000, - purchaseDate: '2024-01-15' - }, - { - id: 2, - coinId: 'ethereum', - symbol: 'ETH', - name: 'Ethereum', - quantity: 3.2, - purchasePrice: 2800, - purchaseDate: '2024-02-20' - } - ] -}; -``` - -### API Endpoints (Mock) -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `/portfolio` | Dashboard page | -| `GET` | `/api/portfolio/holdings` | Get holdings (JSON) | -| `POST` | `/api/portfolio/holdings` | Add holding | -| `PUT` | `/api/portfolio/holdings/:id` | Update holding | -| `DELETE` | `/api/portfolio/holdings/:id` | Delete holding | -| `GET` | `/api/coins/search?q=` | Search coins | - ---- - -## Phase 3: Analytics & Charts UI - -**Goal:** Add portfolio analytics with charts and performance metrics - -### Tasks - -- [ ] Create allocation pie chart (Chart.js) -- [ ] Create portfolio performance line chart -- [ ] Display portfolio metrics cards -- [ ] Show best/worst performing coins -- [ ] Add 24h/7d/30d/All time filters -- [ ] Create portfolio summary section -- [ ] Style analytics section - -### Analytics Dashboard Design -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ PORTFOLIO ANALYTICS │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────┐ ┌─────────────────────────────────┐ │ -│ │ ALLOCATION BY COIN │ │ PORTFOLIO PERFORMANCE │ │ -│ │ │ │ [24H] [7D] [30D] [ALL] │ │ -│ │ ┌─────┐ │ │ │ │ -│ │ ┌──┤ BTC ├──┐ │ │ ╱╲ │ │ -│ │ ╱ │ 55% │ ╲ │ │ ╱╲ ╱ ╲ ╱╲ │ │ -│ │ │ └─────┘ │ │ │ ╱ ╲╱ ╲ ╱ ╲ │ │ -│ │ │ ETH │ SOL │ │ │ ╱ ╳ ╲ │ │ -│ │ ╲ 25% │ 12% ╱ │ │ ╱ ╲ │ │ -│ │ └────┴──────┘ │ │╱───────────────────── │ │ -│ │ BNB: 8% │ │ Jan Feb Mar Apr May │ │ -│ └─────────────────────────────┘ └─────────────────────────────────┘ │ -│ │ -│ ┌───────────────────────────────┐ ┌───────────────────────────────┐ │ -│ │ BEST PERFORMER │ │ WORST PERFORMER │ │ -│ │ ┌────┐ │ │ ┌────┐ │ │ -│ │ │ ◎ │ Solana (SOL) │ │ │ ✕ │ Ripple (XRP) │ │ -│ │ └────┘ +125.5% 🚀 │ │ └────┘ -12.3% 📉 │ │ -│ └───────────────────────────────┘ └───────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ PERFORMANCE METRICS │ │ -│ ├──────────────┬──────────────┬──────────────┬────────────────────┤ │ -│ │ 24H Change │ 7D Change │ 30D Change │ All Time │ │ -│ │ +2.4% │ +8.7% │ +15.2% │ +67.5% │ │ -│ └──────────────┴──────────────┴──────────────┴────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### Metrics to Calculate -| Metric | Formula | -|--------|---------| -| Total Value | Σ (quantity × current_price) | -| Total Invested | Σ (quantity × purchase_price) | -| Total P/L | Total Value - Total Invested | -| P/L % | (Total P/L / Total Invested) × 100 | -| Allocation % | (coin_value / total_value) × 100 | - ---- - -## Phase 4: Watchlist UI - -**Goal:** Create watchlist feature to track coins without owning them - -### Tasks - -- [ ] Create `/views/portfolio/watchlist.ejs` - Watchlist page -- [ ] Add watchlist to navigation -- [ ] Create "Add to Watchlist" functionality -- [ ] Display watchlist with current prices -- [ ] Add price alert settings (UI only, no notifications yet) -- [ ] Add "Quick Add to Portfolio" from watchlist -- [ ] Remove from watchlist functionality - -### New Files -``` -├── views/portfolio/watchlist.ejs -└── public/js/watchlist.js -``` - -### Watchlist Design -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ MY WATCHLIST [+ Add Coin] │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────────────┐ │ -│ │ WATCHING │ │ -│ ├────────┬───────────┬───────────┬───────────┬─────────────────────┤ │ -│ │ Coin │ Price │ 24h % │ 7d % │ Actions │ │ -│ ├────────┼───────────┼───────────┼───────────┼─────────────────────┤ │ -│ │ ◎ SOL │ $145.23 │ +5.2% │ +12.8% │ 🔔 ➕ 🗑️ │ │ -│ │ ⬡ AVAX │ $35.67 │ -2.1% │ +8.4% │ 🔔 ➕ 🗑️ │ │ -│ │ ● DOT │ $7.89 │ +1.8% │ -3.2% │ 🔔 ➕ 🗑️ │ │ -│ └────────┴───────────┴───────────┴───────────┴─────────────────────┘ │ -│ │ -│ 🔔 = Set Alert ➕ = Add to Portfolio 🗑️ = Remove │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### Price Alert Modal -``` -┌────────────────────────────────────────────┐ -│ Set Price Alert - Solana (SOL) [X] │ -├────────────────────────────────────────────┤ -│ │ -│ Current Price: $145.23 │ -│ │ -│ Alert me when price goes: │ -│ │ -│ ☐ Above ┌────────────────────────────┐ │ -│ │ $ 150.00 │ │ -│ └────────────────────────────┘ │ -│ │ -│ ☐ Below ┌────────────────────────────┐ │ -│ │ $ 130.00 │ │ -│ └────────────────────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────────┐ │ -│ │ Cancel │ │ Set Alert │ │ -│ └──────────────┘ └──────────────────┘ │ -│ │ -└────────────────────────────────────────────┘ -``` - ---- - -## Phase 5: Database Integration - -**Goal:** Replace all mock data with MongoDB database - -### Tasks - -- [ ] Set up MongoDB Atlas cluster -- [ ] Create `.env` file with connection string -- [ ] Install database dependencies -- [ ] Create database connection module -- [ ] Create User model with password hashing -- [ ] Create Portfolio model -- [ ] Create Watchlist model -- [ ] Migrate authentication to use database -- [ ] Migrate portfolio to use database -- [ ] Migrate watchlist to use database -- [ ] Add input validation -- [ ] Test all functionality with real data -- [ ] Deploy to production - -### Dependencies for Phase 5 -```bash -npm install mongoose bcryptjs connect-mongo express-validator -``` - -### Environment Variables -```env -# Server -PORT=3000 -NODE_ENV=development - -# MongoDB -MONGODB_URI=mongodb+srv://:@cluster.mongodb.net/cryptopulse - -# Session -SESSION_SECRET=your-super-secret-key-here -``` - -### Database Schema - -#### Users Collection -```javascript -const userSchema = new mongoose.Schema({ - name: { type: String, required: true }, - email: { type: String, required: true, unique: true }, - password: { type: String, required: true }, // Hashed with bcrypt - createdAt: { type: Date, default: Date.now }, - settings: { - currency: { type: String, default: 'usd' }, - theme: { type: String, default: 'dark' } - } -}); -``` - -#### Portfolio Collection -```javascript -const portfolioSchema = new mongoose.Schema({ - userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, - holdings: [{ - coinId: String, - symbol: String, - name: String, - quantity: Number, - purchasePrice: Number, - purchaseDate: Date, - notes: String, - addedAt: { type: Date, default: Date.now } - }], - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now } -}); -``` - -#### Watchlist Collection -```javascript -const watchlistSchema = new mongoose.Schema({ - userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, - coins: [{ - coinId: String, - symbol: String, - name: String, - addedAt: { type: Date, default: Date.now }, - priceAlert: { - above: Number, - below: Number - } - }] -}); -``` - -### Migration Steps -1. Create MongoDB Atlas account and cluster -2. Get connection string -3. Create models -4. Update auth routes to use User model -5. Update portfolio routes to use Portfolio model -6. Update watchlist routes to use Watchlist model -7. Test thoroughly -8. Remove mock data - ---- - -## File Structure (Final) - -``` -cryptipulse/ -├── app.js # Main server file -├── package.json # Dependencies -├── .env # Environment variables (Phase 5) -├── .env.example # Example env file -│ -├── config/ -│ └── database.js # MongoDB connection (Phase 5) -│ -├── models/ # Mongoose models (Phase 5) -│ ├── User.js -│ ├── Portfolio.js -│ └── Watchlist.js -│ -├── middleware/ -│ ├── auth.js # Authentication check (Phase 2) -│ └── validation.js # Input validation (Phase 5) -│ -├── routes/ -│ ├── auth.js # Auth routes (Phase 1) -│ ├── portfolio.js # Portfolio routes (Phase 2) -│ └── api.js # API routes (Phase 2) -│ -├── public/ -│ ├── css/ -│ │ ├── styles.css -│ │ ├── other.css -│ │ ├── auth.css # Phase 1 -│ │ └── portfolio.css # Phase 2 -│ └── js/ -│ ├── markets.js -│ ├── auth.js # Phase 1 -│ ├── portfolio.js # Phase 2 -│ └── watchlist.js # Phase 4 -│ -└── views/ - ├── index.ejs - ├── graphs.ejs - ├── auth/ # Phase 1 - │ ├── login.ejs - │ └── register.ejs - ├── portfolio/ # Phase 2-4 - │ ├── dashboard.ejs - │ └── watchlist.ejs - └── partials/ - ├── header.ejs # Updated in Phase 1 - └── footer.ejs -``` - ---- - -## Quick Reference - -### Phase 1 Commands -```bash -npm install express-session dotenv -``` - -### Phase 5 Commands -```bash -npm install mongoose bcryptjs connect-mongo express-validator -``` - -### Start Development -```bash -npm run dev -``` - ---- - -## Summary - -| Phase | Focus | Key Deliverables | -|-------|-------|------------------| -| **Phase 1** | Auth UI | Login, Register, Sessions, Navigation | -| **Phase 2** | Portfolio UI | Dashboard, Add/Edit/Delete Holdings | -| **Phase 3** | Analytics UI | Charts, Metrics, Performance | -| **Phase 4** | Watchlist UI | Track Coins, Price Alerts | -| **Phase 5** | Database | MongoDB, Real Data, Production Ready | - ---- - -*Document created: January 2025* -*Approach: UI-First, Database-Last* +# CryptoPulse Portfolio Management Feature + +## Overview + +Add user authentication and portfolio management capabilities to CryptoPulse, allowing users to track their cryptocurrency investments, analyze performance, and make informed investment decisions. + +**Approach:** Build UI/Frontend first with mock data, then integrate database at the end. + +--- + +## Table of Contents + +1. [Feature Summary](#feature-summary) +2. [Implementation Phases](#implementation-phases) +3. [Phase Details](#phase-details) +4. [File Structure](#file-structure) +5. [Database Schema (Phase 5)](#database-schema-phase-5) + +--- + +## Feature Summary + +| Feature | Description | +|---------|-------------| +| **User Registration** | Create account with email/password | +| **User Login** | Secure authentication with sessions | +| **Portfolio Dashboard** | View all holdings, total value, profit/loss | +| **Add Holdings** | Add coins with purchase price, quantity, date | +| **Edit/Delete Holdings** | Modify or remove existing holdings | +| **Portfolio Analytics** | Charts, allocation breakdown, performance metrics | +| **Watchlist** | Track coins without owning them | + +--- + +## Implementation Phases + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ IMPLEMENTATION ROADMAP │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ PHASE 1 PHASE 2 PHASE 3 PHASE 4 PHASE 5│ +│ ──────── ──────── ──────── ──────── ────────│ +│ │ +│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ +│ │ Auth │──────▶│ Port- │──────▶│ Anal- │──────▶│ Watch │──────▶│ DB │ │ +│ │ UI │ │ folio │ │ ytics │ │ list │ │ Integ │ │ +│ │ │ │ UI │ │ UI │ │ UI │ │ │ │ +│ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘ │ +│ │ +│ • Login page • Dashboard • Pie chart • Watchlist • MongoDB │ +│ • Register • Add holding • Line chart • Price alerts • User model│ +│ • Mock auth • Holdings • Metrics • Mock data • Portfolio │ +│ • Sessions • Edit/Delete • Summary • UI complete • Migrate │ +│ • Mock data • Mock data • Go live │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Phase Details + +--- + +## Phase 1: Authentication UI + +**Goal:** Create login/register pages with mock authentication (no database yet) + +### Tasks + +- [ ] Create `/views/auth/login.ejs` - Login page +- [ ] Create `/views/auth/register.ejs` - Registration page +- [ ] Create `/public/css/auth.css` - Auth page styling +- [ ] Create `/routes/auth.js` - Auth routes +- [ ] Implement mock authentication (hardcoded user or localStorage) +- [ ] Set up express-session for session management +- [ ] Update header navigation (show Login/Logout based on session) +- [ ] Create user dropdown menu when logged in + +### New Files +``` +├── routes/auth.js +├── views/auth/ +│ ├── login.ejs +│ └── register.ejs +├── public/css/auth.css +└── public/js/auth.js +``` + +### Login Page Design +``` +┌────────────────────────────────────────────────────────┐ +│ CryptoPulse │ +├────────────────────────────────────────────────────────┤ +│ │ +│ Welcome Back │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ 📧 Email │ │ +│ └─────────────────────────┘ │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ 🔒 Password │ │ +│ └─────────────────────────┘ │ +│ │ +│ ☐ Remember me │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ LOGIN │ │ +│ └─────────────────────────┘ │ +│ │ +│ Don't have an account? Register │ +│ │ +└────────────────────────────────────────────────────────┘ +``` + +### Register Page Design +``` +┌────────────────────────────────────────────────────────┐ +│ CryptoPulse │ +├────────────────────────────────────────────────────────┤ +│ │ +│ Create Account │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ 👤 Full Name │ │ +│ └─────────────────────────┘ │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ 📧 Email │ │ +│ └─────────────────────────┘ │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ 🔒 Password │ │ +│ └─────────────────────────┘ │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ 🔒 Confirm Password │ │ +│ └─────────────────────────┘ │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ REGISTER │ │ +│ └─────────────────────────┘ │ +│ │ +│ Already have an account? Login │ +│ │ +└────────────────────────────────────────────────────────┘ +``` + +### Updated Navigation +``` +(Logged Out) +┌──────────────────────────────────────────────────────────────────┐ +│ CryptoPulse │ Home │ Markets │ News │ About │ Login │ +└──────────────────────────────────────────────────────────────────┘ + +(Logged In) +┌──────────────────────────────────────────────────────────────────┐ +│ CryptoPulse │ Home │ Markets │ Portfolio │ News │ About │ User▼│ +└──────────────────────────────────────────────────────────────────┘ + ├─ Profile + ├─ Watchlist + └─ Logout +``` + +### Dependencies for Phase 1 +```bash +npm install express-session dotenv +``` + +### Mock Authentication Logic +```javascript +// Temporary mock user (will be replaced by DB in Phase 5) +const mockUsers = [ + { + id: 1, + name: 'Demo User', + email: 'demo@cryptopulse.com', + password: 'demo123' // In Phase 5, this will be hashed + } +]; +``` + +--- + +## Phase 2: Portfolio Dashboard UI + +**Goal:** Create portfolio dashboard with add/edit/delete functionality using mock data + +### Tasks + +- [ ] Create `/views/portfolio/dashboard.ejs` - Main portfolio page +- [ ] Create `/public/css/portfolio.css` - Portfolio styling +- [ ] Create `/public/js/portfolio.js` - Portfolio interactions +- [ ] Create `/routes/portfolio.js` - Portfolio routes +- [ ] Build holdings table component +- [ ] Create "Add Holding" modal with coin search +- [ ] Implement edit holding functionality +- [ ] Implement delete holding with confirmation +- [ ] Calculate and display portfolio metrics (mock data) +- [ ] Create authentication middleware (protect portfolio routes) + +### New Files +``` +├── routes/portfolio.js +├── middleware/auth.js +├── views/portfolio/ +│ └── dashboard.ejs +├── public/css/portfolio.css +└── public/js/portfolio.js +``` + +### Portfolio Dashboard Design +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PORTFOLIO DASHBOARD [+ Add Holding] │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Total Value │ │ Total Invested │ │ Profit/Loss │ │ +│ │ $45,230.50 │ │ $38,500.00 │ │ +$6,730 (+17%) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ MY HOLDINGS │ │ +│ ├────────┬────────┬───────────┬───────────┬──────────┬─────────────┤ │ +│ │ Coin │ Amount │ Avg Price │ Current │ P/L │ Actions │ │ +│ ├────────┼────────┼───────────┼───────────┼──────────┼─────────────┤ │ +│ │ ₿ BTC │ 0.5 │ $42,000 │ $67,000 │ +59.5% │ ✏️ 🗑️ │ │ +│ │ ⟠ ETH │ 3.2 │ $2,800 │ $3,500 │ +25.0% │ ✏️ 🗑️ │ │ +│ │ ◎ SOL │ 15 │ $85 │ $145 │ +70.5% │ ✏️ 🗑️ │ │ +│ │ ⬡ BNB │ 2 │ $320 │ $580 │ +81.2% │ ✏️ 🗑️ │ │ +│ └────────┴────────┴───────────┴───────────┴──────────┴─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Add Holding Modal Design +``` +┌────────────────────────────────────────────┐ +│ Add New Holding [X] │ +├────────────────────────────────────────────┤ +│ │ +│ Search Coin │ +│ ┌──────────────────────────────────────┐ │ +│ │ 🔍 Search for a coin... │ │ +│ └──────────────────────────────────────┘ │ +│ ┌────────────────────────────────────┐ │ +│ │ ₿ Bitcoin (BTC) │ │ +│ │ ⟠ Ethereum (ETH) │ │ +│ │ ◎ Solana (SOL) │ │ +│ └────────────────────────────────────┘ │ +│ │ +│ Quantity │ +│ ┌──────────────────────────────────────┐ │ +│ │ 0.00 │ │ +│ └──────────────────────────────────────┘ │ +│ │ +│ Purchase Price (per coin) │ +│ ┌──────────────────────────────────────┐ │ +│ │ $ 0.00 [Use Current Price] │ │ +│ └──────────────────────────────────────┘ │ +│ │ +│ Purchase Date │ +│ ┌──────────────────────────────────────┐ │ +│ │ 📅 Select date │ │ +│ └──────────────────────────────────────┘ │ +│ │ +│ Notes (optional) │ +│ ┌──────────────────────────────────────┐ │ +│ │ │ │ +│ └──────────────────────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────────┐ │ +│ │ Cancel │ │ Add Holding │ │ +│ └──────────────┘ └──────────────────┘ │ +│ │ +└────────────────────────────────────────────┘ +``` + +### Mock Portfolio Data +```javascript +// Temporary mock data (will be replaced by DB in Phase 5) +const mockPortfolio = { + holdings: [ + { + id: 1, + coinId: 'bitcoin', + symbol: 'BTC', + name: 'Bitcoin', + quantity: 0.5, + purchasePrice: 42000, + purchaseDate: '2024-01-15' + }, + { + id: 2, + coinId: 'ethereum', + symbol: 'ETH', + name: 'Ethereum', + quantity: 3.2, + purchasePrice: 2800, + purchaseDate: '2024-02-20' + } + ] +}; +``` + +### API Endpoints (Mock) +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/portfolio` | Dashboard page | +| `GET` | `/api/portfolio/holdings` | Get holdings (JSON) | +| `POST` | `/api/portfolio/holdings` | Add holding | +| `PUT` | `/api/portfolio/holdings/:id` | Update holding | +| `DELETE` | `/api/portfolio/holdings/:id` | Delete holding | +| `GET` | `/api/coins/search?q=` | Search coins | + +--- + +## Phase 3: Analytics & Charts UI + +**Goal:** Add portfolio analytics with charts and performance metrics + +### Tasks + +- [ ] Create allocation pie chart (Chart.js) +- [ ] Create portfolio performance line chart +- [ ] Display portfolio metrics cards +- [ ] Show best/worst performing coins +- [ ] Add 24h/7d/30d/All time filters +- [ ] Create portfolio summary section +- [ ] Style analytics section + +### Analytics Dashboard Design +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PORTFOLIO ANALYTICS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ ALLOCATION BY COIN │ │ PORTFOLIO PERFORMANCE │ │ +│ │ │ │ [24H] [7D] [30D] [ALL] │ │ +│ │ ┌─────┐ │ │ │ │ +│ │ ┌──┤ BTC ├──┐ │ │ ╱╲ │ │ +│ │ ╱ │ 55% │ ╲ │ │ ╱╲ ╱ ╲ ╱╲ │ │ +│ │ │ └─────┘ │ │ │ ╱ ╲╱ ╲ ╱ ╲ │ │ +│ │ │ ETH │ SOL │ │ │ ╱ ╳ ╲ │ │ +│ │ ╲ 25% │ 12% ╱ │ │ ╱ ╲ │ │ +│ │ └────┴──────┘ │ │╱───────────────────── │ │ +│ │ BNB: 8% │ │ Jan Feb Mar Apr May │ │ +│ └─────────────────────────────┘ └─────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────┐ ┌───────────────────────────────┐ │ +│ │ BEST PERFORMER │ │ WORST PERFORMER │ │ +│ │ ┌────┐ │ │ ┌────┐ │ │ +│ │ │ ◎ │ Solana (SOL) │ │ │ ✕ │ Ripple (XRP) │ │ +│ │ └────┘ +125.5% 🚀 │ │ └────┘ -12.3% 📉 │ │ +│ └───────────────────────────────┘ └───────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ PERFORMANCE METRICS │ │ +│ ├──────────────┬──────────────┬──────────────┬────────────────────┤ │ +│ │ 24H Change │ 7D Change │ 30D Change │ All Time │ │ +│ │ +2.4% │ +8.7% │ +15.2% │ +67.5% │ │ +│ └──────────────┴──────────────┴──────────────┴────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Metrics to Calculate +| Metric | Formula | +|--------|---------| +| Total Value | Σ (quantity × current_price) | +| Total Invested | Σ (quantity × purchase_price) | +| Total P/L | Total Value - Total Invested | +| P/L % | (Total P/L / Total Invested) × 100 | +| Allocation % | (coin_value / total_value) × 100 | + +--- + +## Phase 4: Watchlist UI + +**Goal:** Create watchlist feature to track coins without owning them + +### Tasks + +- [ ] Create `/views/portfolio/watchlist.ejs` - Watchlist page +- [ ] Add watchlist to navigation +- [ ] Create "Add to Watchlist" functionality +- [ ] Display watchlist with current prices +- [ ] Add price alert settings (UI only, no notifications yet) +- [ ] Add "Quick Add to Portfolio" from watchlist +- [ ] Remove from watchlist functionality + +### New Files +``` +├── views/portfolio/watchlist.ejs +└── public/js/watchlist.js +``` + +### Watchlist Design +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ MY WATCHLIST [+ Add Coin] │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ WATCHING │ │ +│ ├────────┬───────────┬───────────┬───────────┬─────────────────────┤ │ +│ │ Coin │ Price │ 24h % │ 7d % │ Actions │ │ +│ ├────────┼───────────┼───────────┼───────────┼─────────────────────┤ │ +│ │ ◎ SOL │ $145.23 │ +5.2% │ +12.8% │ 🔔 ➕ 🗑️ │ │ +│ │ ⬡ AVAX │ $35.67 │ -2.1% │ +8.4% │ 🔔 ➕ 🗑️ │ │ +│ │ ● DOT │ $7.89 │ +1.8% │ -3.2% │ 🔔 ➕ 🗑️ │ │ +│ └────────┴───────────┴───────────┴───────────┴─────────────────────┘ │ +│ │ +│ 🔔 = Set Alert ➕ = Add to Portfolio 🗑️ = Remove │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Price Alert Modal +``` +┌────────────────────────────────────────────┐ +│ Set Price Alert - Solana (SOL) [X] │ +├────────────────────────────────────────────┤ +│ │ +│ Current Price: $145.23 │ +│ │ +│ Alert me when price goes: │ +│ │ +│ ☐ Above ┌────────────────────────────┐ │ +│ │ $ 150.00 │ │ +│ └────────────────────────────┘ │ +│ │ +│ ☐ Below ┌────────────────────────────┐ │ +│ │ $ 130.00 │ │ +│ └────────────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────────┐ │ +│ │ Cancel │ │ Set Alert │ │ +│ └──────────────┘ └──────────────────┘ │ +│ │ +└────────────────────────────────────────────┘ +``` + +--- + +## Phase 5: Database Integration + +**Goal:** Replace all mock data with MongoDB database + +### Tasks + +- [ ] Set up MongoDB Atlas cluster +- [ ] Create `.env` file with connection string +- [ ] Install database dependencies +- [ ] Create database connection module +- [ ] Create User model with password hashing +- [ ] Create Portfolio model +- [ ] Create Watchlist model +- [ ] Migrate authentication to use database +- [ ] Migrate portfolio to use database +- [ ] Migrate watchlist to use database +- [ ] Add input validation +- [ ] Test all functionality with real data +- [ ] Deploy to production + +### Dependencies for Phase 5 +```bash +npm install mongoose bcryptjs connect-mongo express-validator +``` + +### Environment Variables +```env +# Server +PORT=3000 +NODE_ENV=development + +# MongoDB +MONGODB_URI=mongodb+srv://:@cluster.mongodb.net/cryptopulse + +# Session +SESSION_SECRET=your-super-secret-key-here +``` + +### Database Schema + +#### Users Collection +```javascript +const userSchema = new mongoose.Schema({ + name: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, // Hashed with bcrypt + createdAt: { type: Date, default: Date.now }, + settings: { + currency: { type: String, default: 'usd' }, + theme: { type: String, default: 'dark' } + } +}); +``` + +#### Portfolio Collection +```javascript +const portfolioSchema = new mongoose.Schema({ + userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + holdings: [{ + coinId: String, + symbol: String, + name: String, + quantity: Number, + purchasePrice: Number, + purchaseDate: Date, + notes: String, + addedAt: { type: Date, default: Date.now } + }], + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now } +}); +``` + +#### Watchlist Collection +```javascript +const watchlistSchema = new mongoose.Schema({ + userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + coins: [{ + coinId: String, + symbol: String, + name: String, + addedAt: { type: Date, default: Date.now }, + priceAlert: { + above: Number, + below: Number + } + }] +}); +``` + +### Migration Steps +1. Create MongoDB Atlas account and cluster +2. Get connection string +3. Create models +4. Update auth routes to use User model +5. Update portfolio routes to use Portfolio model +6. Update watchlist routes to use Watchlist model +7. Test thoroughly +8. Remove mock data + +--- + +## File Structure (Final) + +``` +cryptipulse/ +├── app.js # Main server file +├── package.json # Dependencies +├── .env # Environment variables (Phase 5) +├── .env.example # Example env file +│ +├── config/ +│ └── database.js # MongoDB connection (Phase 5) +│ +├── models/ # Mongoose models (Phase 5) +│ ├── User.js +│ ├── Portfolio.js +│ └── Watchlist.js +│ +├── middleware/ +│ ├── auth.js # Authentication check (Phase 2) +│ └── validation.js # Input validation (Phase 5) +│ +├── routes/ +│ ├── auth.js # Auth routes (Phase 1) +│ ├── portfolio.js # Portfolio routes (Phase 2) +│ └── api.js # API routes (Phase 2) +│ +├── public/ +│ ├── css/ +│ │ ├── styles.css +│ │ ├── other.css +│ │ ├── auth.css # Phase 1 +│ │ └── portfolio.css # Phase 2 +│ └── js/ +│ ├── markets.js +│ ├── auth.js # Phase 1 +│ ├── portfolio.js # Phase 2 +│ └── watchlist.js # Phase 4 +│ +└── views/ + ├── index.ejs + ├── graphs.ejs + ├── auth/ # Phase 1 + │ ├── login.ejs + │ └── register.ejs + ├── portfolio/ # Phase 2-4 + │ ├── dashboard.ejs + │ └── watchlist.ejs + └── partials/ + ├── header.ejs # Updated in Phase 1 + └── footer.ejs +``` + +--- + +## Quick Reference + +### Phase 1 Commands +```bash +npm install express-session dotenv +``` + +### Phase 5 Commands +```bash +npm install mongoose bcryptjs connect-mongo express-validator +``` + +### Start Development +```bash +npm run dev +``` + +--- + +## Summary + +| Phase | Focus | Key Deliverables | +|-------|-------|------------------| +| **Phase 1** | Auth UI | Login, Register, Sessions, Navigation | +| **Phase 2** | Portfolio UI | Dashboard, Add/Edit/Delete Holdings | +| **Phase 3** | Analytics UI | Charts, Metrics, Performance | +| **Phase 4** | Watchlist UI | Track Coins, Price Alerts | +| **Phase 5** | Database | MongoDB, Real Data, Production Ready | + +--- + +*Document created: January 2025* +*Approach: UI-First, Database-Last* diff --git a/docs/superpowers/plans/2026-06-17-frontend-redesign.md b/docs/superpowers/plans/2026-06-17-frontend-redesign.md new file mode 100644 index 0000000..5cbaf12 --- /dev/null +++ b/docs/superpowers/plans/2026-06-17-frontend-redesign.md @@ -0,0 +1,2134 @@ +# CryptiPulse Frontend Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rebuild CryptiPulse's frontend with a unified dark amber design system, restructured home page, full Markets page redesign, and scroll-triggered blur+fade animations across all pages. + +**Architecture:** A new `public/css/theme.css` holds all CSS variables (dark-only, all tokens in `:root`). A new `public/js/observer.js` drives all scroll animations via IntersectionObserver. Each page's existing CSS file is updated to use variables. EJS views are rebuilt section by section, all sharing the theme foundation from Task 1. + +**Tech Stack:** EJS templates, plain CSS custom properties, vanilla JavaScript (IntersectionObserver API), Bootstrap 5 CDN (nav dropdown JS only), Google Fonts Inter, Lottie Player CDN (`@lottiefiles/lottie-player`), Lucide SVG icons (inlined as `` strings) + +## Global Constraints + +- Dark mode only — all token values in `:root` directly, no `.dark` class, no toggle +- `@theme inline` block NOT included — plain CSS stack, not Tailwind +- `public/css/theme.css` must be the **first** `` stylesheet in every page `` +- `public/js/observer.js` must be loaded **after all other scripts** in every page `` +- Inter font: `https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap` +- Lottie Player: `https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js` +- Animation classes: `.animate-in` (initial hidden state) → `.animate-in.visible` (shown) +- Count-up attributes: `class="count-up"` + `data-target=""` + `data-suffix=""` +- All git commits without Co-Authored-By tag +- Dev server: `npm run dev` (runs from repo root) +- Base path: `/mnt/c/Users/KING_/Documents/WebDev Projects/Capstone 4/cryptipulse` + +--- + +### Task 1: Design System Foundation + +**Files:** +- Create: `public/css/theme.css` +- Create: `public/js/observer.js` +- Modify: `public/css/about.css` +- Modify: `public/css/chart.css` + +**Interfaces:** +- Produces: all CSS custom properties used by every subsequent task; `.animate-in` / `.visible` class contract; `data-target` / `data-suffix` attributes for count-up elements + +- [ ] **Step 1: Create `public/css/theme.css`** + +```css +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +:root { + --card: #262626; + --ring: #f59e0b; + --input: #404040; + --muted: #262626; + --accent: #92400e; + --border: #404040; + --chart-1: #fbbf24; + --chart-2: #d97706; + --chart-3: #92400e; + --chart-4: #b45309; + --chart-5: #92400e; + --popover: #262626; + --primary: #f59e0b; + --sidebar: #0f0f0f; + --secondary: #262626; + --background: #171717; + --foreground: #e5e5e5; + --destructive: #ef4444; + --sidebar-ring: #f59e0b; + --sidebar-accent: #92400e; + --sidebar-border: #404040; + --card-foreground: #e5e5e5; + --sidebar-primary: #f59e0b; + --muted-foreground: #a3a3a3; + --accent-foreground: #fde68a; + --popover-foreground: #e5e5e5; + --primary-foreground: #000000; + --sidebar-foreground: #e5e5e5; + --secondary-foreground: #e5e5e5; + --destructive-foreground: #ffffff; + --sidebar-accent-foreground: #fde68a; + --sidebar-primary-foreground: #ffffff; + --radius: 0.375rem; + --font-sans: Inter, system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; + --color-up: #22c55e; + --color-down: #ef4444; +} + +*, *::before, *::after { + box-sizing: border-box; +} + +body { + background-color: var(--background); + color: var(--foreground); + font-family: var(--font-sans); + margin: 0; + padding: 0; +} + +.animate-in { + filter: blur(4px); + transform: translateY(-8px); + opacity: 0; + transition: filter 0.8s ease, transform 0.8s ease, opacity 0.8s ease; +} + +.animate-in.visible { + filter: blur(0); + transform: translateY(0); + opacity: 1; +} +``` + +- [ ] **Step 2: Create `public/js/observer.js`** + +```javascript +(function () { + var animObserver = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + animObserver.unobserve(entry.target); + } + }); + }, + { threshold: 0.1 } + ); + + document.querySelectorAll('.animate-in').forEach(function (el) { + animObserver.observe(el); + }); + + var countObserver = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + countUp(entry.target); + countObserver.unobserve(entry.target); + } + }); + }, + { threshold: 0.5 } + ); + + document.querySelectorAll('.count-up').forEach(function (el) { + countObserver.observe(el); + }); + + function countUp(el) { + var target = parseInt(el.dataset.target, 10); + var suffix = el.dataset.suffix || ''; + var duration = 1500; + var start = performance.now(); + function update(now) { + var elapsed = now - start; + var progress = Math.min(elapsed / duration, 1); + var eased = 1 - Math.pow(1 - progress, 3); + el.textContent = Math.floor(eased * target).toLocaleString() + suffix; + if (progress < 1) requestAnimationFrame(update); + } + requestAnimationFrame(update); + } +})(); +``` + +- [ ] **Step 3: Update `public/css/about.css` — replace hardcoded colors with variables** + +Replace the entire file with: + +```css +body { + background-color: var(--background); + font-family: var(--font-sans); + color: var(--foreground); +} + +h1, h2, h3 { + color: var(--foreground); +} + +p { + font-size: 18px; + color: var(--muted-foreground); +} + +.highlight-orange { + color: var(--primary); +} + +.custom-card { + background-color: var(--card); + border-radius: 20px; + padding: 2rem; + color: var(--card-foreground); + border: 1px solid var(--border); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; +} + +.custom-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); +} + +.section-title { + font-size: 2.25rem; + font-weight: 700; + color: var(--foreground); + margin-bottom: 0.5rem; +} + +.section-subtitle { + font-size: 1.125rem; + color: var(--muted-foreground); + max-width: 42rem; + margin-left: auto; + margin-right: auto; +} + +.solCard { + margin-left: 30%; + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +.bull { + margin-top: 50px; +} +``` + +- [ ] **Step 4: Update `public/css/chart.css` — replace hardcoded colors with variables** + +Make these replacements throughout `chart.css`: +- `#1e2a3a` → `var(--card)` +- `#30363d` → `var(--border)` +- `#ffffff` → `var(--foreground)` +- `#8b949e` → `var(--muted-foreground)` +- `#6e7681` → `var(--muted-foreground)` +- `#fd7e14` → `var(--primary)` +- `#e55a00` → `#d97706` +- `#2ecc71` → `var(--color-up)` +- `rgba(46, 204, 113, 0.15)` → `rgba(34, 197, 94, 0.15)` +- `#e74c3c` → `var(--color-down)` +- `rgba(231, 76, 60, 0.15)` → `rgba(239, 68, 68, 0.15)` +- `rgba(13, 27, 42, 0.5)` → `rgba(23, 23, 23, 0.5)` +- `rgba(253, 126, 20, 0.1)` → `rgba(245, 158, 11, 0.1)` + +Also update the `.time-btn.active` gradient: +```css +.time-btn.active { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-foreground); +} +``` + +- [ ] **Step 5: Verify files exist** + +```bash +ls "public/css/theme.css" "public/js/observer.js" +``` + +Expected: both listed without error. + +- [ ] **Step 6: Commit** + +```bash +git add public/css/theme.css public/js/observer.js public/css/about.css public/css/chart.css +git commit -m "feat: add design system tokens, animation observer, update about/chart css" +``` + +--- + +### Task 2: Navbar Rebuild + +**Files:** +- Modify: `views/partials/header.ejs` (full rebuild) +- Modify: `public/css/other.css` (full replace) + +**Interfaces:** +- Consumes: `var(--sidebar)`, `var(--primary)`, `var(--primary-foreground)`, `var(--foreground)`, `var(--muted-foreground)`, `var(--border)`, `var(--popover)`, `var(--radius)` from Task 1 +- Produces: `.site-nav`, `.nav-logo`, `.nav-logo-crypto`, `.nav-logo-pulse`, `.nav-links`, `.nav-link`, `.btn-login`, `.btn-signup`, `.site-footer`, `.footer-inner`, `.footer-brand`, `.footer-links-grid`, `.footer-section` CSS classes used by Task 3 + +- [ ] **Step 1: Rebuild `views/partials/header.ejs`** + +Replace entire file with: + +```html + +``` + +- [ ] **Step 2: Replace `public/css/other.css` with navbar + footer styles** + +Replace entire file with: + +```css +/* ── Navbar ── */ +.site-nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2rem; + height: 64px; + background: var(--sidebar); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 100; +} + +.nav-logo { + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; +} + +.nav-logo-img { + width: 36px; + height: 36px; + object-fit: contain; +} + +.nav-logo-crypto { + font-size: 1.2rem; + font-weight: 700; + color: var(--primary); + letter-spacing: 1px; +} + +.nav-logo-pulse { + font-size: 1.2rem; + font-weight: 700; + color: var(--muted-foreground); + letter-spacing: 1px; +} + +.nav-links { + display: flex; + align-items: center; + gap: 0.25rem; + list-style: none; + margin: 0; + padding: 0; +} + +.nav-link { + color: var(--foreground); + text-decoration: none; + font-size: 0.95rem; + font-weight: 500; + padding: 0.4rem 0.75rem; + border-radius: var(--radius); + transition: color 0.2s ease; + position: relative; +} + +.nav-link:hover { + color: var(--primary); +} + +.nav-link.active { + color: var(--primary); +} + +.nav-link.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0.75rem; + right: 0.75rem; + height: 2px; + background: var(--primary); + border-radius: 1px; +} + +.nav-auth { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.btn-login { + padding: 0.4rem 1rem; + border: 1px solid var(--primary); + border-radius: var(--radius); + color: var(--primary); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + transition: background 0.2s ease; +} + +.btn-login:hover { + background: rgba(245, 158, 11, 0.1); + color: var(--primary); +} + +.btn-signup { + padding: 0.4rem 1rem; + background: var(--primary); + border: 1px solid var(--primary); + border-radius: var(--radius); + color: var(--primary-foreground); + text-decoration: none; + font-size: 0.9rem; + font-weight: 600; + transition: background 0.2s ease, border-color 0.2s ease; +} + +.btn-signup:hover { + background: #d97706; + border-color: #d97706; +} + +.nav-user-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.75rem; + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: 999px; + cursor: pointer; + color: var(--foreground); + font-family: var(--font-sans); +} + +.nav-avatar { + width: 28px; + height: 28px; + background: var(--primary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.8rem; + color: var(--primary-foreground); +} + +.nav-username { + color: var(--foreground); + font-size: 0.9rem; + font-weight: 500; +} + +.nav-dropdown { + background: var(--popover) !important; + border: 1px solid var(--border) !important; +} + +.nav-dropdown .dropdown-item { + color: var(--popover-foreground) !important; + font-size: 0.9rem; +} + +.nav-dropdown .dropdown-item:hover { + background: rgba(245, 158, 11, 0.1) !important; + color: var(--primary) !important; +} + +.nav-divider { + border-color: var(--border) !important; +} + +/* ── Footer ── */ +.site-footer { + border-top: 1px solid var(--border); + background: var(--background); + padding: 3rem 2rem; + position: relative; +} + +.site-footer::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 33%; + height: 1px; + background: var(--foreground); + opacity: 0.2; + filter: blur(4px); +} + +.footer-inner { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 2fr; + gap: 2rem; +} + +.footer-brand { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.footer-logo { + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; +} + +.footer-logo-img { + width: 32px; + height: 32px; + object-fit: contain; +} + +.footer-copy { + color: var(--muted-foreground); + font-size: 0.85rem; + margin: 0; +} + +.footer-links-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 2rem; +} + +.footer-section h3 { + color: var(--foreground); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0 0 1rem 0; +} + +.footer-section ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.footer-section a { + color: var(--muted-foreground); + text-decoration: none; + font-size: 0.9rem; + display: inline-flex; + align-items: center; + gap: 0.35rem; + transition: color 0.3s ease; +} + +.footer-section a:hover { + color: var(--foreground); +} + +.footer-section svg { + width: 15px; + height: 15px; + flex-shrink: 0; +} + +/* ── Responsive ── */ +@media (max-width: 768px) { + .site-nav { + padding: 0 1rem; + } + + .nav-links { + display: none; + } + + .footer-inner { + grid-template-columns: 1fr; + } + + .footer-links-grid { + grid-template-columns: repeat(2, 1fr); + } +} +``` + +- [ ] **Step 3: Start dev server and verify navbar** + +```bash +npm run dev +``` + +Open `http://localhost:3000`. Verify: +- Logo image + "Crypto" (amber) + "Pulse" (gray) on far left +- Home / Market / About nav links in the middle +- Login (outlined amber) and Sign Up (filled amber) on far right +- Navbar sticks to top on scroll +- No large gap above/below the navbar (old 100px padding bug gone) + +- [ ] **Step 4: Commit** + +```bash +git add views/partials/header.ejs public/css/other.css +git commit -m "feat: rebuild navbar with sticky amber dark theme" +``` + +--- + +### Task 3: Footer Rebuild + +**Files:** +- Modify: `views/partials/footer.ejs` (full rebuild) + +**Interfaces:** +- Consumes: `.site-footer`, `.footer-inner`, `.footer-brand`, `.footer-logo`, `.footer-logo-img`, `.footer-copy`, `.footer-links-grid`, `.footer-section`, `.nav-logo-crypto`, `.nav-logo-pulse`, `.animate-in` from Task 2 +- Produces: `footer.ejs` partial used on all pages + +- [ ] **Step 1: Rebuild `views/partials/footer.ejs`** + +Replace entire file with: + +```html + +``` + +- [ ] **Step 2: Verify footer renders on home page** + +With `npm run dev` running, open `http://localhost:3000` and scroll to the bottom. Verify: +- 2-column grid: brand + copyright on left, 4 link columns on right +- "CryptoPulse" logo text in amber/gray +- Product / Company / Resources / Social Links columns +- SVG icons before social link text +- Columns fade in with blur+fade as they scroll into view + +- [ ] **Step 3: Commit** + +```bash +git add views/partials/footer.ejs +git commit -m "feat: rebuild footer with 4-column animated layout and social icons" +``` + +--- + +### Task 4: Home Page Rebuild + +**Files:** +- Modify: `views/index.ejs` (full rebuild) +- Modify: `public/css/styles.css` (full rebuild) + +**Interfaces:** +- Consumes: all CSS variables from Task 1; `.animate-in`, `.count-up` from Task 1; header/footer partials from Tasks 2 & 3; `.btn-login`, `.btn-signup` from Task 2 +- Produces: home page at `/`; CSS classes `.hero-section`, `.stats-section`, `.features-section`, `.testimonials-section`, `.why-section`, `.btn-primary-filled`, `.btn-primary-outline` + +- [ ] **Step 1: Replace `views/index.ejs` with rebuilt home page** + +```html + + + + + + CryptoPulse — Real-time Crypto Data + + + + + + + + +<%- include('partials/header') %> + + +
+
+
+

+ CryptoPulse +

+

+ Real-time data on 20,000+ cryptocurrencies.
+ For Traders, Developers & Enterprises. +

+ +
+
+ + +
+
+
+ + +
+
+
+ 0 + API Calls / Month +
+
+ 0 + Endpoints +
+
+ 0 + Historical Data +
+
+ 0 + Networks +
+
+ 0 + NFT Collections +
+
+ 0 + Marketplaces +
+
+
+ + +
+
+
+ + +
+
+

Everything You Need

+
    +
  • Multi-chain coverage — Bitcoin, Ethereum, BNB, Solana and 200+ more
  • +
  • Real-time prices — live data updated every 60 seconds
  • +
  • Historical data — 10+ years of OHLCV records
  • +
  • NFT floor prices — 2,000+ collections across 30+ marketplaces
  • +
  • Enterprise-grade uptime — built for high-volume API usage
  • +
+
+
+
+ + +
+

What Our Users Say

+ <% + var testimonials = [ + { name: 'Alex M.', role: 'Day Trader', quote: 'CryptoPulse gives me the real-time edge I need. No delays, no excuses.' }, + { name: 'Sarah K.', role: 'DeFi Developer', quote: 'The API is rock solid. 200+ networks in one place is a game-changer.' }, + { name: 'James T.', role: 'Portfolio Manager', quote: 'Finally a dashboard that\'s clean and actually fast. Love the portfolio tracking.' }, + { name: 'Priya N.', role: 'NFT Collector', quote: 'Floor price tracking across 30+ marketplaces? Insane. Nothing else comes close.' }, + { name: 'Carlos R.', role: 'Quant Analyst', quote: '10 years of historical OHLCV data with minute granularity. Exactly what I needed.' }, + { name: 'Emma L.', role: 'Crypto Blogger', quote: 'I embed CryptoPulse data in every post. My readers trust the numbers.' }, + { name: 'David W.', role: 'Retail Investor', quote: 'Set up my watchlist in minutes. The UI is so much cleaner than the big sites.' }, + { name: 'Yuki H.', role: 'Blockchain Eng.', quote: 'Free tier is generous and the docs are actually good. Rare combination.' } + ]; + %> +
+
+ <% for (var r = 0; r < 2; r++) { testimonials.forEach(function(t) { %> +
+
+
<%= t.name.charAt(0) %>
+
+
<%= t.name %>
+
<%= t.role %>
+
+
+
★★★★★
+

"<%= t.quote %>"

+
+ <% }); } %> +
+
+ <% for (var r2 = 0; r2 < 2; r2++) { testimonials.forEach(function(t) { %> +
+
+
<%= t.name.charAt(0) %>
+
+
<%= t.name %>
+
<%= t.role %>
+
+
+
★★★★★
+

"<%= t.quote %>"

+
+ <% }); } %> +
+
+
+ + +
+

Why Choose CryptoPulse?

+
+
+

Others

+
    +
  • Delayed or cached price data
  • +
  • Cluttered, hard-to-navigate UI
  • +
  • No built-in portfolio tracking
  • +
  • Key features behind paywalls
  • +
  • No watchlist functionality
  • +
+
+
+

CryptoPulse

+
    +
  • Real-time prices, updated live
  • +
  • Clean, intuitive dashboard
  • +
  • Full portfolio tracking built in
  • +
  • Free data access, no paywalls
  • +
  • Custom watchlists for any coin
  • +
+
+
+
+ +<%- include('partials/footer') %> + + + + + + + +``` + +- [ ] **Step 2: Replace `public/css/styles.css` with home page styles** + +```css +/* ── Hero ── */ +.hero-section { + padding: 5rem 2rem; + background: var(--background); + background-image: radial-gradient(ellipse 60% 50% at 70% 50%, rgba(245, 158, 11, 0.12), transparent); + overflow: hidden; +} + +.hero-inner { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 55% 45%; + align-items: center; + gap: 2rem; +} + +.hero-heading { + font-size: clamp(2.5rem, 5vw, 4rem); + font-weight: 700; + line-height: 1.1; + margin: 0 0 1.5rem 0; +} + +.hero-crypto { + color: var(--primary); + display: inline-block; +} + +.hero-pulse { + color: var(--muted-foreground); + display: inline-block; +} + +.hero-sub { + color: var(--muted-foreground); + font-size: 1.15rem; + line-height: 1.6; + margin: 0 0 2rem 0; +} + +.hero-actions { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.btn-primary-filled { + padding: 0.65rem 1.5rem; + background: var(--primary); + color: var(--primary-foreground); + border: none; + border-radius: var(--radius); + font-size: 1rem; + font-weight: 600; + text-decoration: none; + display: inline-block; + transition: background 0.2s ease, transform 0.2s ease; +} + +.btn-primary-filled:hover { + background: #d97706; + transform: translateY(-2px); + color: var(--primary-foreground); +} + +.btn-primary-outline { + padding: 0.65rem 1.5rem; + background: transparent; + color: var(--primary); + border: 1px solid var(--primary); + border-radius: var(--radius); + font-size: 1rem; + font-weight: 600; + text-decoration: none; + display: inline-block; + transition: background 0.2s ease; +} + +.btn-primary-outline:hover { + background: rgba(245, 158, 11, 0.1); + color: var(--primary); +} + +.hero-lottie { + display: flex; + justify-content: center; + align-items: center; +} + +/* ── Stats ── */ +.stats-section { + padding: 4rem 2rem; + background: var(--muted); +} + +.stats-inner { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 1rem; +} + +.stat-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem 1rem; + text-align: center; + display: flex; + flex-direction: column; + gap: 0.5rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.stat-card:hover { + border-color: var(--primary); + box-shadow: 0 0 16px rgba(245, 158, 11, 0.15); +} + +.stat-number { + font-size: 1.8rem; + font-weight: 700; + color: var(--primary); + display: block; +} + +.stat-label { + font-size: 0.75rem; + color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.05em; + display: block; +} + +/* ── Features ── */ +.features-section { + padding: 5rem 2rem; + background: var(--background); +} + +.features-inner { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 45% 55%; + align-items: center; + gap: 3rem; +} + +.features-heading { + font-size: 2rem; + font-weight: 700; + color: var(--foreground); + margin: 0 0 2rem 0; +} + +.bullet-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.bullet-list li { + color: var(--foreground); + font-size: 1rem; + line-height: 1.5; + display: flex; + align-items: flex-start; + gap: 0.75rem; +} + +.bullet-icon { + color: var(--primary); + flex-shrink: 0; + font-size: 1rem; + margin-top: 2px; +} + +/* ── Testimonials ── */ +.testimonials-section { + padding: 5rem 0; + background: var(--muted); + overflow: hidden; +} + +.section-heading { + text-align: center; + font-size: 2rem; + font-weight: 700; + color: var(--foreground); + margin: 0 0 3rem 0; + padding: 0 2rem; +} + +.marquee-wrapper { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.marquee-wrapper:hover .marquee-track { + animation-play-state: paused; +} + +.marquee-track { + display: flex; + gap: 1rem; + width: max-content; + animation: marquee-left 40s linear infinite; +} + +.marquee-track.reverse { + animation: marquee-right 40s linear infinite; +} + +@keyframes marquee-left { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} + +@keyframes marquee-right { + from { transform: translateX(-50%); } + to { transform: translateX(0); } +} + +.testimonial-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem; + width: 280px; + flex-shrink: 0; +} + +.testimonial-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.testimonial-avatar { + width: 36px; + height: 36px; + background: var(--primary); + color: var(--primary-foreground); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.9rem; + flex-shrink: 0; +} + +.testimonial-name { + font-weight: 600; + color: var(--foreground); + font-size: 0.9rem; +} + +.testimonial-role { + font-size: 0.8rem; + color: var(--muted-foreground); +} + +.testimonial-stars { + color: var(--primary); + font-size: 0.85rem; + margin-bottom: 0.75rem; +} + +.testimonial-quote { + color: var(--muted-foreground); + font-size: 0.875rem; + line-height: 1.5; + margin: 0; +} + +/* ── Why Us ── */ +.why-section { + padding: 5rem 2rem; + background: var(--background); +} + +.why-inner { + max-width: 900px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; +} + +.why-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2rem; +} + +.why-us { + border-color: var(--primary); + box-shadow: 0 0 24px rgba(245, 158, 11, 0.15); +} + +.why-card-title { + font-size: 1rem; + font-weight: 700; + color: var(--muted-foreground); + margin: 0 0 1.5rem 0; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.why-us-title { + color: var(--primary); +} + +.why-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.why-list li { + color: var(--foreground); + font-size: 0.95rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.why-x { + color: var(--color-down); + font-weight: 700; + flex-shrink: 0; +} + +.why-check { + color: var(--color-up); + font-weight: 700; + flex-shrink: 0; +} + +/* ── Responsive ── */ +@media (max-width: 1100px) { + .stats-inner { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 768px) { + .hero-inner { + grid-template-columns: 1fr; + } + .hero-lottie { + display: none; + } + .features-inner { + grid-template-columns: 1fr; + } + .features-lottie { + display: none; + } + .why-inner { + grid-template-columns: 1fr; + } + .stats-inner { + grid-template-columns: repeat(2, 1fr); + } +} +``` + +- [ ] **Step 3: Verify home page** + +Open `http://localhost:3000`. Check each section: +- "Crypto" (amber) and "Pulse" (gray) blur+fade in on page load, staggered +- Blockchain Lottie on the right side of hero +- Stats section: 6 `#262626` cards, numbers count up from 0 when scrolled to +- Features section: network Lottie on left, 5 amber ✦ bullet points on right +- Testimonials: two rows scrolling in opposite directions, pause on hover +- Why Us: "Others" card (red ✗) vs "CryptoPulse" card (green ✓, amber border glow) +- Footer below + +- [ ] **Step 4: Commit** + +```bash +git add views/index.ejs public/css/styles.css +git commit -m "feat: rebuild home page with hero, stats, features, testimonials, why-us" +``` + +--- + +### Task 5: Markets Page Rebuild + +**Files:** +- Modify: `views/partials/markets.ejs` (full rebuild) +- Modify: `public/css/markets.css` (full rebuild) +- Modify: `public/js/markets.js` (update row builder + add trending cards) + +**Interfaces:** +- Consumes: CSS variables from Task 1; nav/footer partials from Tasks 2 & 3; `.animate-in` from Task 1 +- Produces: markets page at `/markets`; JS functions `renderTrendingCards(coins)` and `buildRow(coin, index)` internal to `markets.js` + +**Note:** `views/partials/markets.ejs` is a full HTML page (has its own ``) despite being in the partials folder. The existing `markets.js` uses `getElementById("market-data-body")` and `getElementById("loading-indicator")` — keep these IDs. + +- [ ] **Step 1: Rebuild `views/partials/markets.ejs`** + +```html + + + + + + Markets — CryptoPulse + + + + + + + <%- include('header') %> + +
+ +
+

Markets

+ +
+ + + +
+
+
+ +
+ + + + + + + + + + + + + + +
#CoinPrice24h %Market CapVolume (24h)
+
+ +
+ + <%- include('footer') %> + + + + + + + +``` + +- [ ] **Step 2: Replace `public/css/markets.css`** + +```css +.markets-container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.markets-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + flex-wrap: wrap; + gap: 1rem; +} + +.markets-title { + font-size: 2rem; + font-weight: 700; + color: var(--foreground); + margin: 0; +} + +.markets-search { + position: relative; + display: flex; + align-items: center; +} + +.search-icon { + position: absolute; + left: 0.75rem; + width: 16px; + height: 16px; + color: var(--muted-foreground); + pointer-events: none; +} + +.search-input { + padding: 0.5rem 1rem 0.5rem 2.25rem; + background: var(--input); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--foreground); + font-family: var(--font-sans); + font-size: 0.9rem; + outline: none; + width: 240px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.search-input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2); +} + +.search-input::placeholder { + color: var(--muted-foreground); +} + +.trending-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 2rem; +} + +.trending-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + display: flex; + align-items: center; + gap: 1rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.trending-card:hover { + border-color: var(--primary); + box-shadow: 0 0 16px rgba(245, 158, 11, 0.15); +} + +.trending-logo { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; +} + +.trending-info { + flex: 1; + min-width: 0; +} + +.trending-badge { + display: block; + font-size: 0.7rem; + font-weight: 600; + color: var(--primary); + background: rgba(245, 158, 11, 0.15); + padding: 0.15rem 0.5rem; + border-radius: 999px; + width: fit-content; + margin-bottom: 0.3rem; +} + +.trending-name { + font-weight: 600; + color: var(--foreground); + font-size: 0.95rem; +} + +.trending-symbol { + color: var(--muted-foreground); + font-size: 0.8rem; + text-transform: uppercase; +} + +.trending-price { + text-align: right; + flex-shrink: 0; +} + +.trending-price-value { + font-weight: 600; + color: var(--foreground); + font-size: 0.95rem; + display: block; + margin-bottom: 0.25rem; +} + +.market-loading { + display: flex; + justify-content: center; + align-items: center; + height: 200px; +} + +.market-spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(245, 158, 11, 0.2); + border-left-color: var(--primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.market-table-wrapper { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +.market-table { + width: 100%; + border-collapse: collapse; +} + +.market-table thead { + position: sticky; + top: 64px; + z-index: 10; +} + +.market-table th { + padding: 0.875rem 1rem; + background: var(--card); + color: var(--muted-foreground); + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.market-table td { + padding: 0.875rem 1rem; + color: var(--foreground); + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.market-table tbody tr:last-child td { + border-bottom: none; +} + +.market-table tbody tr:hover { + background: rgba(245, 158, 11, 0.04); +} + +.th-rank { + width: 50px; +} + +.td-rank { + color: var(--muted-foreground); + font-weight: 500; +} + +.th-right, .td-right { + text-align: right; +} + +.coin-info { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.coin-info img { + width: 28px; + height: 28px; + border-radius: 50%; +} + +.coin-name { + font-weight: 600; + color: var(--foreground); + font-size: 0.9rem; +} + +.coin-symbol { + color: var(--muted-foreground); + font-size: 0.75rem; + text-transform: uppercase; +} + +.badge-up { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 600; + background: rgba(34, 197, 94, 0.15); + color: var(--color-up); +} + +.badge-down { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 600; + background: rgba(239, 68, 68, 0.15); + color: var(--color-down); +} + +@media (max-width: 768px) { + .trending-cards { + grid-template-columns: 1fr; + } + .markets-header { + flex-direction: column; + align-items: flex-start; + } + .search-input { + width: 100%; + } +} +``` + +- [ ] **Step 3: Replace `public/js/markets.js` with updated version** + +```javascript +document.addEventListener('DOMContentLoaded', function () { + var tableBody = document.getElementById('market-data-body'); + var loadingIndicator = document.getElementById('loading-indicator'); + + function formatCurrency(num) { + if (num === null || num === undefined) return 'N/A'; + return '$' + num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function formatLargeNumber(num) { + if (num === null || num === undefined) return 'N/A'; + if (num >= 1e12) return '$' + (num / 1e12).toFixed(2) + 'T'; + if (num >= 1e9) return '$' + (num / 1e9).toFixed(2) + 'B'; + if (num >= 1e6) return '$' + (num / 1e6).toFixed(2) + 'M'; + return '$' + num.toLocaleString('en-US'); + } + + function renderTrendingCards(coins) { + var container = document.getElementById('trending-cards'); + if (!container) return; + var top3 = coins.slice(0, 3); + container.innerHTML = top3.map(function (coin, i) { + var change = coin.price_change_percentage_24h || 0; + var badgeClass = change >= 0 ? 'badge-up' : 'badge-down'; + var sign = change >= 0 ? '+' : ''; + return ''; + }).join(''); + } + + function buildRow(coin, index) { + var change = coin.price_change_percentage_24h || 0; + var badgeClass = change >= 0 ? 'badge-up' : 'badge-down'; + var sign = change >= 0 ? '+' : ''; + return '' + + '' + (index + 1) + '' + + '
' + + '' + coin.name + '' + + '
' + + '
' + coin.name + '
' + + '
' + coin.symbol.toUpperCase() + '
' + + '
' + + '
' + + '' + formatCurrency(coin.current_price) + '' + + '' + sign + change.toFixed(2) + '%' + + '' + formatLargeNumber(coin.market_cap) + '' + + '' + formatLargeNumber(coin.total_volume) + '' + + ''; + } + + async function fetchMarketData() { + try { + var response = await fetch('/api/markets'); + if (!response.ok) throw new Error('HTTP ' + response.status); + var data = await response.json(); + + loadingIndicator.style.display = 'none'; + + renderTrendingCards(data); + + tableBody.innerHTML = ''; + data.forEach(function (coin, index) { + tableBody.insertAdjacentHTML('beforeend', buildRow(coin, index)); + }); + } catch (error) { + console.error('Error fetching market data:', error); + loadingIndicator.innerHTML = + '

Failed to load market data.

' + + ''; + } + } + + fetchMarketData(); +}); +``` + +- [ ] **Step 4: Verify markets page** + +Open `http://localhost:3000/markets`. Verify: +- Dark `#171717` background, `#0f0f0f` navbar +- "Markets" heading fades in +- Search bar appears in top-right +- Top 3 trending cards load with logo, 🔥 badge, price, % change pill +- Full table loads below with rank, logo+name/symbol, price, 24h% pill, market cap, volume +- Search input filters table rows live (type "bit" → only Bitcoin rows show) +- Green badges for positive %, red for negative +- Page scrolls smoothly with sticky table header + +- [ ] **Step 5: Commit** + +```bash +git add views/partials/markets.ejs public/css/markets.css public/js/markets.js +git commit -m "feat: rebuild markets page with trending cards and searchable table" +``` + +--- + +### Task 6: Auth Pages Update + +**Files:** +- Modify: `views/auth/login.ejs` +- Modify: `views/auth/register.ejs` +- Modify: `public/css/auth.css` + +**Interfaces:** +- Consumes: CSS variables from Task 1; `.animate-in` from Task 1; nav/footer partials from Tasks 2 & 3 +- Produces: login at `/auth/login`, register at `/auth/register` + +- [ ] **Step 1: Update `views/auth/login.ejs` — add `theme.css` first and `observer.js` last, add animation classes** + +In ``, add `theme.css` as the **first** stylesheet: +```html + +``` + +Add `.animate-in` with staggered delays to the form elements. Wrap the `.auth-card` and its children: +```html +
+``` +Add staggered delays to each form group and button inside: +```html +
+ +
+
+ +
+
+ +
+ +``` + +Before ``, add: +```html + +``` + +- [ ] **Step 2: Update `views/auth/register.ejs` — same pattern** + +Add `theme.css` first in ``, `observer.js` before ``. + +Add `.animate-in` with staggered delays on each form group (4 fields: delays 0.1s, 0.2s, 0.3s, 0.4s) and submit button (delay 0.5s). Wrap `.auth-card` with `.animate-in`. + +- [ ] **Step 3: Replace hardcoded colors in `public/css/auth.css`** + +Make these replacements throughout the file: + +| Old value | Replace with | +|-----------|-------------| +| `linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)` | `var(--background)` | +| `#1e2a3a` | `var(--card)` | +| `#30363d` | `var(--border)` | +| `#ffffff` | `var(--foreground)` | +| `#8b949e` | `var(--muted-foreground)` | +| `#c9d1d9` | `var(--card-foreground)` | +| `#0d1b2a` | `var(--input)` | +| `#fd7e14` | `var(--primary)` | +| `#ff9f43` | `#fbbf24` | +| `#e55a00` | `#d97706` | +| `#cc4a00` | `#b45309` | +| `#6e7681` | `var(--muted-foreground)` | +| `rgba(253, 126, 20, 0.2)` | `rgba(245, 158, 11, 0.2)` | +| `rgba(253, 126, 20, 0.1)` | `rgba(245, 158, 11, 0.1)` | +| `rgba(253, 126, 20, 0.3)` | `rgba(245, 158, 11, 0.3)` | +| `rgba(220, 53, 69, 0.15)` | `rgba(239, 68, 68, 0.15)` | +| `rgba(220, 53, 69, 0.3)` | `rgba(239, 68, 68, 0.3)` | +| `rgba(25, 135, 84, 0.15)` | `rgba(34, 197, 94, 0.15)` | +| `rgba(25, 135, 84, 0.3)` | `rgba(34, 197, 94, 0.3)` | + +Also update `.auth-container`: +```css +.auth-container { + min-height: calc(100vh - 64px); + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; + background: var(--background); +} +``` + +- [ ] **Step 4: Verify auth pages** + +Open `http://localhost:3000/auth/login`. Verify: +- Page background is `#171717` (not the old blue gradient) +- Card (`#262626`) fades+blurs in on load +- Form fields stagger in sequentially +- Input fields have `#404040` background, amber focus ring +- Login button is amber with black text +- "Don't have an account?" link is amber + +Open `http://localhost:3000/auth/register`. Same checks, 4 form fields visible. + +- [ ] **Step 5: Commit** + +```bash +git add views/auth/login.ejs views/auth/register.ejs public/css/auth.css +git commit -m "feat: update auth pages with dark theme and staggered animations" +``` + +--- + +### Task 7: Portfolio Dashboard Theme Update + +**Files:** +- Modify: `views/portfolio/dashboard.ejs` +- Modify: `public/css/portfolio.css` + +**Interfaces:** +- Consumes: CSS variables from Task 1; `.animate-in` from Task 1 +- Produces: portfolio dashboard at `/portfolio` + +- [ ] **Step 1: Add `theme.css` as first stylesheet in `views/portfolio/dashboard.ejs`** + +Find the `` section. Add as the very first ``: +```html + +``` + +Before ``, add: +```html + +``` + +Add `.animate-in` class with staggered delays to the three main sections: +```html +
...
+
...
+
...
+``` + +- [ ] **Step 2: Replace hardcoded colors in `public/css/portfolio.css`** + +Make these replacements throughout: + +| Old value | Replace with | +|-----------|-------------| +| `#1e2a3a` | `var(--card)` | +| `#30363d` | `var(--border)` | +| `#ffffff` | `var(--foreground)` | +| `#8b949e` | `var(--muted-foreground)` | +| `#c9d1d9` | `var(--card-foreground)` | +| `#0d1b2a` | `var(--input)` | +| `#fd7e14` | `var(--primary)` | +| `#e55a00` | `#d97706` | +| `#cc4a00` | `#b45309` | +| `#2ecc71` | `var(--color-up)` | +| `#e74c3c` | `var(--color-down)` | +| `#6e7681` | `var(--muted-foreground)` | +| `rgba(253, 126, 20` | `rgba(245, 158, 11` | +| `rgba(46, 204, 113` | `rgba(34, 197, 94` | +| `rgba(231, 76, 60` | `rgba(239, 68, 68` | +| `rgba(13, 27, 42` | `rgba(23, 23, 23` | + +Also update `.portfolio-container` body background override if present: +```css +.portfolio-container { + max-width: 1400px; + margin: 0 auto; + padding: 30px 20px; +} +``` + +If Chart.js chart colors are hardcoded in `dashboard.ejs` script tags, replace with: +```javascript +backgroundColor: [ + getComputedStyle(document.documentElement).getPropertyValue('--chart-1').trim(), + getComputedStyle(document.documentElement).getPropertyValue('--chart-2').trim(), + getComputedStyle(document.documentElement).getPropertyValue('--chart-3').trim(), + getComputedStyle(document.documentElement).getPropertyValue('--chart-4').trim(), + getComputedStyle(document.documentElement).getPropertyValue('--chart-5').trim() +] +``` + +- [ ] **Step 3: Verify portfolio dashboard** + +Log in at `/auth/login`, navigate to `/portfolio`. Verify: +- `#171717` page background +- `#262626` summary cards and holdings section +- Summary cards, holdings table, analytics section fade+blur in on load +- Amber buttons and focus rings +- Green P/L values use `#22c55e`, red losses use `#ef4444` +- No visual regressions in the add/edit/delete holding modals + +- [ ] **Step 4: Commit** + +```bash +git add views/portfolio/dashboard.ejs public/css/portfolio.css +git commit -m "feat: update portfolio dashboard with dark theme and scroll animations" +``` + +--- + +### Task 8: Watchlist Theme Update + +**Files:** +- Modify: `views/portfolio/watchlist.ejs` +- Modify: `public/css/watchlist.css` + +**Interfaces:** +- Consumes: CSS variables from Task 1; `.animate-in` from Task 1 +- Produces: watchlist at `/portfolio/watchlist` + +- [ ] **Step 1: Add `theme.css` as first stylesheet in `views/portfolio/watchlist.ejs`** + +Find the `` section. Add as the very first ``: +```html + +``` + +Before ``, add: +```html + +``` + +Add `.animate-in` to main sections: +```html +
...
+
...
+``` + +- [ ] **Step 2: Replace hardcoded colors in `public/css/watchlist.css`** + +Make these replacements throughout: + +| Old value | Replace with | +|-----------|-------------| +| `#1e2a3a` | `var(--card)` | +| `#30363d` | `var(--border)` | +| `#ffffff` | `var(--foreground)` | +| `#8b949e` | `var(--muted-foreground)` | +| `#c9d1d9` | `var(--card-foreground)` | +| `#0d1b2a` | `var(--input)` | +| `#fd7e14` | `var(--primary)` | +| `#e55a00` | `#d97706` | +| `#cc4a00` | `#b45309` | +| `#2ecc71` | `var(--color-up)` | +| `#e74c3c` | `var(--color-down)` | +| `#f1c40f` | `var(--chart-1)` | +| `rgba(253, 126, 20` | `rgba(245, 158, 11` | +| `rgba(46, 204, 113` | `rgba(34, 197, 94` | +| `rgba(231, 76, 60` | `rgba(239, 68, 68` | +| `rgba(241, 196, 15` | `rgba(251, 191, 36` | +| `rgba(13, 27, 42` | `rgba(23, 23, 23` | + +- [ ] **Step 3: Verify watchlist page** + +Navigate to `/portfolio/watchlist` (must be logged in). Verify: +- Dark theme applied throughout +- Header and table fade+blur in on load +- Change badges show green pill (`.badge-up` / `.badge-down` styled similarly to markets) +- Alert (yellow), Add to Portfolio (green), Remove (red) action buttons styled correctly +- No visual regressions in the alert modal or quick-add modal + +- [ ] **Step 4: Commit** + +```bash +git add views/portfolio/watchlist.ejs public/css/watchlist.css +git commit -m "feat: update watchlist with dark theme and scroll animations" +``` + +--- + +## Self-Review + +**Spec coverage:** + +| Spec requirement | Task | Status | +|---|---|---| +| `theme.css` with all dark tokens in `:root` | Task 1 | ✓ | +| `observer.js` IntersectionObserver animation | Task 1 | ✓ | +| `about.css` color token update | Task 1 | ✓ | +| `chart.css` color token update | Task 1 | ✓ | +| Navbar: logo left, links, login/signup right | Task 2 | ✓ | +| Footer: 4-column animated, SVG social icons | Task 3 | ✓ | +| Home hero: blur+fade heading, text+lottie, amber glow | Task 4 | ✓ | +| Home stats: 6 count-up cards | Task 4 | ✓ | +| Home features: network lottie + bullet list | Task 4 | ✓ | +| Home testimonials: dual infinite marquee | Task 4 | ✓ | +| Home why us: split comparison | Task 4 | ✓ | +| Markets: top 3 trending cards | Task 5 | ✓ | +| Markets: searchable table with badges | Task 5 | ✓ | +| Auth: centered card + staggered animations | Task 6 | ✓ | +| Portfolio: theme + scroll animations | Task 7 | ✓ | +| Watchlist: theme + scroll animations | Task 8 | ✓ | +| `graphs.ejs` needs `theme.css` | — | ⚠️ | + +**Gap:** `views/graphs.ejs` uses `chart.css` but `theme.css` is not added to it. Fix: in Task 7 or Task 8, also add `` as the first stylesheet in `views/graphs.ejs` head. (Add it to Task 7 step 1.) + +**Placeholder scan:** No TBDs found. All steps have concrete code. + +**Type/name consistency:** +- `loadingIndicator` id: `loading-indicator` — consistent between Task 5 HTML and Task 5 JS ✓ +- `trending-cards` id — consistent between Task 5 HTML and `renderTrendingCards()` in JS ✓ +- `.animate-in` / `.visible` — defined Task 1, used Tasks 2-8 ✓ +- `.count-up` / `data-target` / `data-suffix` — defined Task 1 observer, used Task 4 HTML ✓ +- `.badge-up` / `.badge-down` — defined Task 5 CSS, used Task 5 JS ✓ +- `.btn-primary-filled` / `.btn-primary-outline` — defined Task 4 CSS, used Task 4 HTML ✓ +- `.nav-logo-crypto` / `.nav-logo-pulse` — defined Task 2 CSS, reused in footer Task 3 ✓ + +**Fix for graphs.ejs gap:** Add to Task 7, Step 1: also open `views/graphs.ejs` and add `` as the first stylesheet in its ``. diff --git a/docs/superpowers/specs/2026-06-17-frontend-redesign-design.md b/docs/superpowers/specs/2026-06-17-frontend-redesign-design.md new file mode 100644 index 0000000..9a039bc --- /dev/null +++ b/docs/superpowers/specs/2026-06-17-frontend-redesign-design.md @@ -0,0 +1,278 @@ +# CryptiPulse Frontend Redesign — Design Spec + +**Date:** 2026-06-17 +**Status:** Approved +**Scope:** Full frontend visual overhaul — design system, home page restructure, Markets full redesign, all other pages themed + +--- + +## 1. Design System + +### Theme File: `public/css/theme.css` + +Single source of truth for all design tokens. All `.dark` values placed directly in `:root` (dark mode only — no toggle, no light mode). + +```css +:root { + --card: #262626; + --ring: #f59e0b; + --input: #404040; + --muted: #262626; + --accent: #92400e; + --border: #404040; + --chart-1: #fbbf24; + --chart-2: #d97706; + --chart-3: #92400e; + --chart-4: #b45309; + --chart-5: #92400e; + --popover: #262626; + --primary: #f59e0b; + --sidebar: #0f0f0f; + --secondary: #262626; + --background: #171717; + --foreground: #e5e5e5; + --destructive: #ef4444; + --sidebar-ring: #f59e0b; + --sidebar-accent: #92400e; + --sidebar-border: #404040; + --card-foreground: #e5e5e5; + --sidebar-primary: #f59e0b; + --muted-foreground: #a3a3a3; + --accent-foreground: #fde68a; + --popover-foreground: #e5e5e5; + --primary-foreground: #000000; + --sidebar-foreground: #e5e5e5; + --secondary-foreground: #e5e5e5; + --destructive-foreground: #ffffff; + --sidebar-accent-foreground: #fde68a; + --sidebar-primary-foreground: #ffffff; + --radius: 0.375rem; + --font-sans: Inter, sans-serif; + --font-mono: 'JetBrains Mono', monospace; + /* Price change semantic tokens */ + --color-up: #22c55e; + --color-down: #ef4444; +} +``` + +**Font:** Inter loaded from Google Fonts, applied globally via `font-family: var(--font-sans)`. +**`@theme inline` block:** Dropped — Tailwind-only, not used in this EJS/plain-CSS stack. + +### All Existing CSS Files Updated + +Every hardcoded hex color in `styles.css`, `markets.css`, `portfolio.css`, `watchlist.css`, `auth.css`, `other.css`, `chart.css`, `about.css` replaced with the appropriate CSS variable. File structure stays the same — only values change. + +--- + +## 2. Animation System + +All scroll-triggered animations use **Intersection Observer + CSS transitions** (no Framer Motion — this is not a React project). + +### Standard Entrance Animation +```css +.animate-in { + filter: blur(4px); + transform: translateY(-8px); + opacity: 0; + transition: filter 0.8s ease, transform 0.8s ease, opacity 0.8s ease; +} +.animate-in.visible { + filter: blur(0); + transform: translateY(0); + opacity: 1; +} +``` + +A single `observer.js` script applies `IntersectionObserver` to all `.animate-in` elements, adding `.visible` when they enter the viewport. Stagger delays applied via inline `transition-delay` on each element. + +--- + +## 3. Navbar (`views/partials/header.ejs`) + +**Layout:** Logo far left — nav links center-left — auth buttons far right. + +``` +[🟡 CryptoPulse logo + text] Home Dashboard Market About [Login] [Sign Up] +``` + +- Background: `var(--sidebar)` (`#0f0f0f`) with `1px solid var(--border)` bottom +- Logo: existing `cryptologo1.png` + "Crypto" in `var(--primary)` + "Pulse" in `var(--muted-foreground)` +- Nav links: `var(--foreground)`, hover → `var(--primary)`, active → amber underline `2px solid var(--primary)` +- **Login:** outlined button — `border: 1px solid var(--primary)`, `color: var(--primary)`, transparent bg +- **Sign Up:** filled button — `background: var(--primary)`, `color: var(--primary-foreground)` (black) +- Logged-in state: replaces Login/Sign Up with user avatar dropdown (existing behavior, restyled) +- Bootstrap kept only for the dropdown JS behaviour; nav layout converted to plain flexbox CSS + +--- + +## 4. Home Page (`views/index.ejs`) + +Full structural rebuild. Sections in order: + +### 4.1 Hero +- **Layout:** 2-column grid — text left (55%), blockchain Lottie animation right (45%) +- **Background:** `var(--background)` with a radial amber glow: `radial-gradient(ellipse 60% 50% at 70% 50%, rgba(245,158,11,0.12), transparent)` +- **"CryptoPulse" heading:** blur+fade entrance animation on page load (not scroll-triggered) — "Crypto" animates first (delay 0), "Pulse" follows (delay 0.15s) +- **Subheading:** "Real-time data on 20,000+ cryptocurrencies. For Traders, Developers & Enterprises." +- **Buttons:** [Get Started →] (filled amber) | [View Markets] (outlined amber) +- **Lottie:** `blockchain.json`, 500×500, existing `` setup retained + +### 4.2 Stats Section +- **Layout:** 6 cards in a horizontal row, responsive wrap to 3×2 on mobile +- **Card style:** `var(--card)` bg, `var(--border)` border, `var(--radius)` corners, amber glow on hover +- **Number:** large, `var(--primary)` color, count-up animation (0 → final value) triggered by IntersectionObserver +- **Label:** `var(--muted-foreground)` text below the number +- **Stats:** 10B+ API Calls | 70+ Endpoints | 10+ Years Data | 200+ Networks | 2000+ NFTs | 30+ Marketplaces +- **Entrance:** `.animate-in` class, staggered delays 0.1s apart + +### 4.3 Features Section +- **Layout:** 2-column grid — network Lottie left (45%), feature bullets right (55%) +- **Lottie:** `network.json`, 400×400 +- **Bullets:** 5 items, each prefixed with amber `✦` icon + 1. Multi-chain coverage — Bitcoin, Ethereum, BNB, Solana and 200+ more + 2. Real-time prices — live data updated every 60 seconds + 3. Historical data — 10+ years of OHLCV records + 4. NFT floor prices — 2,000+ collections across 30+ marketplaces + 5. Enterprise-grade uptime — built for high-volume API usage +- **Entrance:** section animates in with blur+fade on scroll + +### 4.4 Testimonials Section +- **Layout:** 2 rows of cards in infinite horizontal marquee + - Row 1 scrolls left + - Row 2 scrolls right + - Pauses on hover (`animation-play-state: paused`) +- **Card:** `var(--card)` bg, `var(--border)` border, avatar initial circle in amber, name + role, star rating (5 amber stars), quote in `var(--muted-foreground)` +- **Content:** 8 mock testimonials (fictional traders/developers), duplicated to fill the loop +- **Section heading:** "What Our Users Say" — animate-in on scroll + +### 4.5 Why Us Section +- **Layout:** 2 columns side by side, full-width section +- **Left card — "Others":** `var(--card)` bg, `var(--destructive)` ✗ marks, `var(--muted-foreground)` text + - ✗ Delayed or cached price data + - ✗ Cluttered, hard-to-navigate UI + - ✗ No built-in portfolio tracking + - ✗ Key features behind paywalls + - ✗ No watchlist functionality +- **Right card — "CryptoPulse":** `var(--card)` bg, `2px solid var(--primary)` border, amber glow, `var(--color-up)` ✓ marks, `var(--foreground)` text + - ✓ Real-time prices, updated live + - ✓ Clean, intuitive dashboard + - ✓ Full portfolio tracking built in + - ✓ Free data access, no paywalls + - ✓ Custom watchlists for any coin +- **Entrance:** both cards animate in with blur+fade, right card with slight delay + +### 4.6 Footer (`views/partials/footer.ejs`) +- **Layout:** 3-column grid — logo+copyright left (1/3), 4-column link grid right (2/3) +- **Top border:** `1px solid var(--border)` with a blurred radial glow line centered at the top +- **Logo column:** CryptoPulse logo + "© 2026 CryptoPulse. All rights reserved." in `var(--muted-foreground)` +- **Link columns:** Product | Company | Resources | Social Links + - Product: Features (#features), Markets (/markets), Portfolio (/portfolio), Graphs (/graphs) + - Company: About (/about), Privacy Policy (#), Terms of Service (#) + - Resources: Help (#), Changelog (#) + - Social Links: Facebook, Instagram, Youtube, LinkedIn (with SVG icons from Lucide CDN) +- **Link style:** `var(--muted-foreground)`, hover → `var(--foreground)`, 0.3s transition +- **Entrance:** each column animates in with blur+fade, staggered 0.1s per column +- **Social icons:** Lucide SVG icons inlined directly as `` strings in the EJS template (no CDN dependency) + +--- + +## 5. Markets Page (`views/partials/markets.ejs`) + +Full structural rebuild. + +### 5.1 Page Header +- Title "Markets" in `var(--foreground)`, large +- Live search input (right-aligned): filters table rows in real-time via JS +- Input style: `var(--input)` bg, `var(--border)` border, amber focus ring + +### 5.2 Trending Cards (Top 3) +- 3 cards in a horizontal row showing the top 3 coins by 24h volume +- **Card:** `var(--card)` bg, `var(--border)` border, amber glow on hover, `border-radius: var(--radius)` +- **Card content:** 🔥 "Trending" badge (amber), coin logo (32px circle), coin name + symbol, current price in `var(--foreground)`, 24h% change badge (green/red) +- **Entrance:** 3 cards animate in with blur+fade, staggered 0.1s apart + +### 5.3 Market Table +- Columns: `#` (rank) | Coin (logo + name + symbol) | Price | 24h% | Market Cap | Volume +- **Header row:** `var(--muted-foreground)` text, `var(--border)` bottom border, sticky on scroll +- **Data rows:** `var(--background)` base, alternating `var(--card)` for even rows, hover → amber left border `2px` +- **% change badges:** inline pill — green (`var(--color-up)`) bg at 15% opacity + text for positive, red for negative +- **Coin cell:** 32px circular logo + bold name + muted symbol below +- **Populated by:** existing `markets.js` fetch from CoinGecko API — markup updated, JS data binding updated to match new HTML structure +- **Search:** filters visible rows live without re-fetching +- **Entrance:** table animates in as a unit with blur+fade on load + +--- + +## 6. Auth Pages (`views/auth/login.ejs`, `views/auth/register.ejs`) + +### Layout +Centered card on `var(--background)` page background. + +``` +Page bg: #171717 + └── Card: #262626, border: var(--border), border-radius: 12px, max-width: 420px + ├── CryptoPulse logo + title + ├── Form fields (staggered blur+fade, each with 0.1s delay increment) + └── Submit button (amber filled) +``` + +- **Inputs:** `var(--input)` bg, `var(--border)` border, amber focus ring (`box-shadow: 0 0 0 3px rgba(245,158,11,0.2)`) +- **Submit button:** `var(--primary)` bg, `var(--primary-foreground)` text, hover darkens to `#d97706` +- **Links:** `var(--primary)` color, hover underline +- **Error alerts:** `var(--destructive)` border at 30% opacity, matching bg tint +- **Entrance:** entire card fades+blurs in on page load; form fields stagger in sequentially + +--- + +## 7. Portfolio Dashboard (`views/portfolio/dashboard.ejs`) + +No structural changes. Updates: +- All hardcoded colors → CSS variables +- Summary cards, holdings table, analytics cards, modal: all get `.animate-in` class +- Blur+fade entrance triggered by IntersectionObserver on page load +- Chart colors updated to use `--chart-1` through `--chart-5` +- Green P/L → `var(--color-up)`, red P/L → `var(--color-down)` + +--- + +## 8. Watchlist Page (`views/portfolio/watchlist.ejs`) + +No structural changes. Same updates as Portfolio: +- All hardcoded colors → CSS variables +- Watchlist table, empty state, action buttons: `.animate-in` added +- Change badges use `var(--color-up)` / `var(--color-down)` + +--- + +## 9. File Changes Summary + +| File | Change Type | +|------|-------------| +| `public/css/theme.css` | **NEW** — design token file | +| `public/js/observer.js` | **NEW** — IntersectionObserver animation script | +| `views/partials/header.ejs` | **REBUILD** — new nav layout | +| `views/partials/footer.ejs` | **REBUILD** — 4-column animated footer | +| `views/index.ejs` | **REBUILD** — all 6 sections | +| `views/partials/markets.ejs` | **REBUILD** — cards + table layout | +| `views/auth/login.ejs` | **UPDATE** — themed, animated | +| `views/auth/register.ejs` | **UPDATE** — themed, animated | +| `views/portfolio/dashboard.ejs` | **UPDATE** — tokens + animations | +| `views/portfolio/watchlist.ejs` | **UPDATE** — tokens + animations | +| `public/css/styles.css` | **UPDATE** — hardcoded colors → variables | +| `public/css/markets.css` | **UPDATE** — hardcoded colors → variables | +| `public/css/portfolio.css` | **UPDATE** — hardcoded colors → variables | +| `public/css/watchlist.css` | **UPDATE** — hardcoded colors → variables | +| `public/css/auth.css` | **UPDATE** — hardcoded colors → variables | +| `public/css/other.css` | **UPDATE** — hardcoded colors → variables, fix 100px padding bug | +| `public/css/about.css` | **UPDATE** — hardcoded colors → variables | +| `public/css/chart.css` | **UPDATE** — hardcoded colors → variables | + +--- + +## 10. Out of Scope + +- No light mode toggle +- No changes to backend routes, API calls, or session logic +- No changes to Portfolio/Watchlist page structure or functionality +- No new pages added +- `@theme inline` Tailwind block not used diff --git a/middleware/auth.js b/middleware/auth.js index 375a272..a0f0b3c 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,22 +1,22 @@ -// Authentication middleware - -// Check if user is logged in -const isAuthenticated = (req, res, next) => { - if (req.session.user) { - return next(); - } - res.redirect('/auth/login'); -}; - -// Check if user is NOT logged in (for login/register pages) -const isNotAuthenticated = (req, res, next) => { - if (!req.session.user) { - return next(); - } - res.redirect('/portfolio'); -}; - -module.exports = { - isAuthenticated, - isNotAuthenticated -}; +// Authentication middleware + +// Check if user is logged in +const isAuthenticated = (req, res, next) => { + if (req.session.user) { + return next(); + } + res.redirect('/auth/login'); +}; + +// Check if user is NOT logged in (for login/register pages) +const isNotAuthenticated = (req, res, next) => { + if (!req.session.user) { + return next(); + } + res.redirect('/portfolio'); +}; + +module.exports = { + isAuthenticated, + isNotAuthenticated +}; diff --git a/public/css/about.css b/public/css/about.css index 343c0db..ee53e6d 100644 --- a/public/css/about.css +++ b/public/css/about.css @@ -1,64 +1,59 @@ -@import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@700&display=swap'); - -/* Your Custom Styles - Integrated */ body { - background-color: #323539; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - color: #C9D1D9; + background-color: var(--background); + font-family: var(--font-sans); + color: var(--foreground); } h1, h2, h3 { - color: #FFFFFF; + color: var(--foreground); } p { - font-size: 18px; - color: #C9D1D9; + font-size: 18px; + color: var(--muted-foreground); } .highlight-orange { - color: #fd7e14; + color: var(--primary); } -/* Card styles from your homepage CSS */ .custom-card { - background-color: #161b22; /* Darker card background for contrast */ - border-radius: 20px; - padding: 2rem; - color: #C9D1D9; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); - transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; + background-color: var(--card); + border-radius: 20px; + padding: 2rem; + color: var(--card-foreground); + border: 1px solid var(--border); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; } .custom-card:hover { - transform: translateY(-5px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); -} + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); +} -/* Section Title styles adapted from your CSS */ .section-title { - font-size: 2.25rem; /* Equivalent to text-3xl */ - font-weight: 700; - color: #FFFFFF; - margin-bottom: 0.5rem; + font-size: 2.25rem; + font-weight: 700; + color: var(--foreground); + margin-bottom: 0.5rem; } .section-subtitle { - font-size: 1.125rem; /* Equivalent to text-lg */ - color: #C9D1D9; - max-width: 42rem; /* max-w-2xl */ - margin-left: auto; - margin-right: auto; + font-size: 1.125rem; + color: var(--muted-foreground); + max-width: 42rem; + margin-left: auto; + margin-right: auto; } -.solCard{ - margin-left: 30%; - display: grid; - grid-template-columns: 1fr; - gap: 1.5rem; +.solCard { + margin-left: 30%; + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; } -.bull{ - margin-top: 50px; -} \ No newline at end of file +.bull { + margin-top: 50px; +} diff --git a/public/css/auth.css b/public/css/auth.css index b8439a9..324a539 100644 --- a/public/css/auth.css +++ b/public/css/auth.css @@ -1,216 +1,216 @@ -/* Auth Pages Styling */ - -.auth-container { - min-height: calc(100vh - 200px); - display: flex; - align-items: center; - justify-content: center; - padding: 40px 20px; - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); -} - -.auth-card { - background: #1e2a3a; - border-radius: 20px; - padding: 40px; - width: 100%; - max-width: 420px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - border: 1px solid rgba(255, 255, 255, 0.1); -} - -.auth-header { - text-align: center; - margin-bottom: 30px; -} - -.auth-header h1 { - color: #ffffff; - font-size: 2rem; - font-weight: 700; - margin-bottom: 10px; -} - -.auth-header p { - color: #8b949e; - font-size: 1rem; - margin: 0; -} - -.auth-form { - display: flex; - flex-direction: column; - gap: 20px; -} - -.form-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.form-group label { - color: #c9d1d9; - font-size: 0.9rem; - font-weight: 500; -} - -.input-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.input-icon { - position: absolute; - left: 15px; - color: #8b949e; - display: flex; - align-items: center; - justify-content: center; - pointer-events: none; -} - -.input-wrapper .form-control { - width: 100%; - padding: 14px 14px 14px 50px; - background: #0d1b2a; - border: 1px solid #30363d; - border-radius: 10px; - color: #ffffff; - font-size: 1rem; - transition: all 0.3s ease; -} - -.input-wrapper .form-control::placeholder { - color: #6e7681; -} - -.input-wrapper .form-control:focus { - outline: none; - border-color: #fd7e14; - box-shadow: 0 0 0 3px rgba(253, 126, 20, 0.2); - background: #0d1b2a; -} - -.form-options { - display: flex; - justify-content: space-between; - align-items: center; -} - -.remember-me { - display: flex; - align-items: center; - gap: 8px; - color: #8b949e; - font-size: 0.9rem; - cursor: pointer; -} - -.remember-me input[type="checkbox"] { - width: 16px; - height: 16px; - accent-color: #fd7e14; - cursor: pointer; -} - -.btn-auth { - width: 100%; - padding: 14px; - background: linear-gradient(135deg, #fd7e14 0%, #e55a00 100%); - border: none; - border-radius: 10px; - color: #ffffff; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; - margin-top: 10px; -} - -.btn-auth:hover { - background: linear-gradient(135deg, #e55a00 0%, #cc4a00 100%); - transform: translateY(-2px); - box-shadow: 0 10px 30px rgba(253, 126, 20, 0.3); -} - -.btn-auth:active { - transform: translateY(0); -} - -.auth-footer { - text-align: center; - margin-top: 25px; - padding-top: 20px; - border-top: 1px solid #30363d; -} - -.auth-footer p { - color: #8b949e; - margin: 0; -} - -.auth-footer a { - color: #fd7e14; - text-decoration: none; - font-weight: 500; - transition: color 0.3s ease; -} - -.auth-footer a:hover { - color: #ff9f43; - text-decoration: underline; -} - -/* Alert styling */ -.alert { - padding: 12px 16px; - border-radius: 10px; - margin-bottom: 20px; - font-size: 0.9rem; -} - -.alert-danger { - background: rgba(220, 53, 69, 0.15); - border: 1px solid rgba(220, 53, 69, 0.3); - color: #f8d7da; -} - -.alert-success { - background: rgba(25, 135, 84, 0.15); - border: 1px solid rgba(25, 135, 84, 0.3); - color: #d1e7dd; -} - -/* Demo credentials box */ -.demo-credentials { - margin-top: 20px; - padding: 15px; - background: rgba(253, 126, 20, 0.1); - border: 1px solid rgba(253, 126, 20, 0.3); - border-radius: 10px; - text-align: center; -} - -.demo-credentials p { - margin: 5px 0; - color: #c9d1d9; - font-size: 0.85rem; -} - -.demo-credentials p:first-child { - color: #fd7e14; - margin-bottom: 10px; -} - -/* Responsive */ -@media (max-width: 480px) { - .auth-card { - padding: 30px 20px; - } - - .auth-header h1 { - font-size: 1.7rem; - } -} +/* Auth Pages Styling */ + +.auth-container { + min-height: calc(100vh - 64px); + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; + background: var(--background); +} + +.auth-card { + background: var(--card); + border-radius: 20px; + padding: 40px; + width: 100%; + max-width: 420px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.auth-header { + text-align: center; + margin-bottom: 30px; +} + +.auth-header h1 { + color: var(--foreground); + font-size: 2rem; + font-weight: 700; + margin-bottom: 10px; +} + +.auth-header p { + color: var(--muted-foreground); + font-size: 1rem; + margin: 0; +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-group label { + color: var(--card-foreground); + font-size: 0.9rem; + font-weight: 500; +} + +.input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.input-icon { + position: absolute; + left: 15px; + color: var(--muted-foreground); + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.input-wrapper .form-control { + width: 100%; + padding: 14px 14px 14px 50px; + background: var(--input); + border: 1px solid var(--border); + border-radius: 10px; + color: var(--foreground); + font-size: 1rem; + transition: all 0.3s ease; +} + +.input-wrapper .form-control::placeholder { + color: var(--muted-foreground); +} + +.input-wrapper .form-control:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2); + background: var(--input); +} + +.form-options { + display: flex; + justify-content: space-between; + align-items: center; +} + +.remember-me { + display: flex; + align-items: center; + gap: 8px; + color: var(--muted-foreground); + font-size: 0.9rem; + cursor: pointer; +} + +.remember-me input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--primary); + cursor: pointer; +} + +.btn-auth { + width: 100%; + padding: 14px; + background: var(--primary); + border: none; + border-radius: 10px; + color: var(--primary-foreground); + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + margin-top: 10px; +} + +.btn-auth:hover { + background: #d97706; + transform: translateY(-2px); + box-shadow: 0 10px 30px rgba(245, 158, 11, 0.3); +} + +.btn-auth:active { + transform: translateY(0); +} + +.auth-footer { + text-align: center; + margin-top: 25px; + padding-top: 20px; + border-top: 1px solid var(--border); +} + +.auth-footer p { + color: var(--muted-foreground); + margin: 0; +} + +.auth-footer a { + color: var(--primary); + text-decoration: none; + font-weight: 500; + transition: color 0.3s ease; +} + +.auth-footer a:hover { + color: #fbbf24; + text-decoration: underline; +} + +/* Alert styling */ +.alert { + padding: 12px 16px; + border-radius: 10px; + margin-bottom: 20px; + font-size: 0.9rem; +} + +.alert-danger { + background: rgba(239, 68, 68, 0.15); + border: 1px solid rgba(239, 68, 68, 0.3); + color: #f8d7da; +} + +.alert-success { + background: rgba(34, 197, 94, 0.15); + border: 1px solid rgba(34, 197, 94, 0.3); + color: #d1e7dd; +} + +/* Demo credentials box */ +.demo-credentials { + margin-top: 20px; + padding: 15px; + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: 10px; + text-align: center; +} + +.demo-credentials p { + margin: 5px 0; + color: var(--card-foreground); + font-size: 0.85rem; +} + +.demo-credentials p:first-child { + color: var(--primary); + margin-bottom: 10px; +} + +/* Responsive */ +@media (max-width: 480px) { + .auth-card { + padding: 30px 20px; + } + + .auth-header h1 { + font-size: 1.7rem; + } +} diff --git a/public/css/chart.css b/public/css/chart.css index 5227ef0..b9e34dd 100644 --- a/public/css/chart.css +++ b/public/css/chart.css @@ -1,713 +1,713 @@ -/* Charts Page Styles */ - -.charts-page { - max-width: 1400px; - margin: 0 auto; - padding: 30px 20px 60px; - min-height: calc(100vh - 200px); -} - -.charts-header { - text-align: center; - margin-bottom: 40px; -} - -.charts-header h1 { - color: #ffffff; - font-size: 2.2rem; - font-weight: 700; - margin: 0 0 10px 0; -} - -.charts-header p { - color: #8b949e; - font-size: 1.1rem; - margin: 0; -} - -/* Charts Grid - Desktop first, then responsive */ -.charts-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 25px; -} - -/* Chart Card */ -.chart-card { - background: #1e2a3a; - border-radius: 16px; - border: 1px solid #30363d; - overflow: hidden; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.chart-card:hover { - transform: translateY(-2px); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); -} - -/* Card Header */ -.chart-card-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 24px; - border-bottom: 1px solid #30363d; - flex-wrap: wrap; - gap: 12px; -} - -.coin-title { - display: flex; - align-items: center; - gap: 12px; -} - -.coin-logo { - width: 40px; - height: 40px; - border-radius: 50%; - flex-shrink: 0; -} - -.coin-title h3 { - color: #ffffff; - font-size: 1.25rem; - font-weight: 600; - margin: 0; -} - -.coin-symbol { - color: #8b949e; - font-size: 0.85rem; - display: block; -} - -.price-info { - text-align: right; -} - -.current-price { - color: #ffffff; - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 4px; - line-height: 1.2; -} - -.price-change { - font-size: 0.9rem; - font-weight: 600; - padding: 4px 10px; - border-radius: 6px; - display: inline-block; -} - -.price-change.positive { - color: #2ecc71; - background: rgba(46, 204, 113, 0.15); -} - -.price-change.negative { - color: #e74c3c; - background: rgba(231, 76, 60, 0.15); -} - -/* Chart Stats */ -.chart-stats { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1px; - background: #30363d; - border-bottom: 1px solid #30363d; -} - -.stat { - background: #1e2a3a; - padding: 12px 8px; - text-align: center; -} - -.stat-label { - display: block; - color: #8b949e; - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.3px; - margin-bottom: 4px; - white-space: nowrap; -} - -.stat-value { - display: block; - color: #ffffff; - font-size: 0.85rem; - font-weight: 600; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Time Filters */ -.time-filters { - display: flex; - gap: 6px; - padding: 12px 16px; - border-bottom: 1px solid #30363d; - flex-wrap: wrap; - justify-content: flex-start; -} - -.time-btn { - padding: 8px 14px; - background: transparent; - border: 1px solid #30363d; - border-radius: 8px; - color: #8b949e; - font-size: 0.8rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - flex-shrink: 0; -} - -.time-btn:hover { - border-color: #fd7e14; - color: #fd7e14; - background: rgba(253, 126, 20, 0.1); -} - -.time-btn.active { - background: linear-gradient(135deg, #fd7e14 0%, #e55a00 100%); - border-color: #fd7e14; - color: #ffffff; -} - -/* Chart Container */ -.chart-container { - position: relative; - height: 280px; - padding: 16px; -} - -.price-chart { - width: 100% !important; - height: 100% !important; -} - -/* Loading State */ -.chart-loading { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 12px; - color: #8b949e; - font-size: 0.9rem; - background: #1e2a3a; -} - -.spinner { - width: 32px; - height: 32px; - border: 3px solid #30363d; - border-top-color: #fd7e14; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.chart-loading .error { - color: #e74c3c; -} - -/* Chart Footer */ -.chart-footer { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 16px; - background: rgba(13, 27, 42, 0.5); - border-top: 1px solid #30363d; -} - -.chart-info { - display: flex; - align-items: center; - gap: 8px; - color: #8b949e; - font-size: 0.75rem; -} - -.info-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: #2ecc71; - flex-shrink: 0; -} - -.data-source { - color: #6e7681; - font-size: 0.7rem; -} - -/* ============================================ - RESPONSIVE BREAKPOINTS - ============================================ */ - -/* Large Desktop (1400px+) */ -@media (min-width: 1400px) { - .charts-grid { - grid-template-columns: repeat(2, 1fr); - gap: 30px; - } - - .chart-container { - height: 320px; - } -} - -/* Desktop / Laptop (1024px - 1399px) */ -@media (max-width: 1399px) and (min-width: 1024px) { - .charts-grid { - grid-template-columns: repeat(2, 1fr); - gap: 20px; - } - - .chart-card-header { - padding: 16px 20px; - } - - .chart-container { - height: 260px; - } -} - -/* Tablet Landscape (768px - 1023px) */ -@media (max-width: 1023px) and (min-width: 768px) { - .charts-page { - padding: 25px 15px 50px; - } - - .charts-header h1 { - font-size: 1.9rem; - } - - .charts-grid { - grid-template-columns: 1fr; - gap: 20px; - max-width: 700px; - margin: 0 auto; - } - - .chart-card-header { - padding: 18px 20px; - } - - .coin-title h3 { - font-size: 1.2rem; - } - - .current-price { - font-size: 1.4rem; - } - - .chart-stats { - grid-template-columns: repeat(4, 1fr); - } - - .stat { - padding: 12px 10px; - } - - .stat-label { - font-size: 0.7rem; - } - - .stat-value { - font-size: 0.85rem; - } - - .chart-container { - height: 280px; - padding: 16px 20px; - } -} - -/* Tablet Portrait (600px - 767px) */ -@media (max-width: 767px) and (min-width: 600px) { - .charts-page { - padding: 20px 15px 40px; - } - - .charts-header h1 { - font-size: 1.7rem; - } - - .charts-header p { - font-size: 1rem; - } - - .charts-grid { - grid-template-columns: 1fr; - gap: 18px; - } - - .chart-card-header { - padding: 16px 18px; - } - - .coin-logo { - width: 36px; - height: 36px; - } - - .coin-title h3 { - font-size: 1.1rem; - } - - .current-price { - font-size: 1.3rem; - } - - .chart-stats { - grid-template-columns: repeat(4, 1fr); - } - - .stat { - padding: 10px 6px; - } - - .stat-label { - font-size: 0.65rem; - } - - .stat-value { - font-size: 0.8rem; - } - - .time-filters { - padding: 10px 14px; - gap: 6px; - } - - .time-btn { - padding: 7px 12px; - font-size: 0.75rem; - } - - .chart-container { - height: 260px; - padding: 14px; - } -} - -/* Mobile Large (480px - 599px) */ -@media (max-width: 599px) and (min-width: 480px) { - .charts-page { - padding: 15px 12px 35px; - } - - .charts-header { - margin-bottom: 25px; - } - - .charts-header h1 { - font-size: 1.5rem; - } - - .charts-header p { - font-size: 0.9rem; - } - - .charts-grid { - grid-template-columns: 1fr; - gap: 15px; - } - - .chart-card { - border-radius: 12px; - } - - .chart-card:hover { - transform: none; - } - - .chart-card-header { - flex-direction: row; - justify-content: space-between; - padding: 14px 16px; - gap: 10px; - } - - .coin-logo { - width: 32px; - height: 32px; - } - - .coin-title { - gap: 10px; - } - - .coin-title h3 { - font-size: 1rem; - } - - .coin-symbol { - font-size: 0.75rem; - } - - .price-info { - text-align: right; - } - - .current-price { - font-size: 1.15rem; - } - - .price-change { - font-size: 0.75rem; - padding: 3px 8px; - } - - .chart-stats { - grid-template-columns: repeat(2, 1fr); - } - - .stat { - padding: 10px 12px; - } - - .stat-label { - font-size: 0.65rem; - } - - .stat-value { - font-size: 0.8rem; - } - - .time-filters { - padding: 10px 12px; - gap: 5px; - justify-content: space-between; - } - - .time-btn { - padding: 6px 10px; - font-size: 0.7rem; - flex: 1; - text-align: center; - } - - .chart-container { - height: 220px; - padding: 12px; - } - - .chart-footer { - padding: 8px 12px; - } - - .chart-info { - font-size: 0.7rem; - } - - .data-source { - font-size: 0.65rem; - } -} - -/* Mobile Small (320px - 479px) */ -@media (max-width: 479px) { - .charts-page { - padding: 12px 10px 30px; - } - - .charts-header { - margin-bottom: 20px; - } - - .charts-header h1 { - font-size: 1.3rem; - } - - .charts-header p { - font-size: 0.85rem; - } - - .charts-grid { - grid-template-columns: 1fr; - gap: 12px; - } - - .chart-card { - border-radius: 10px; - } - - .chart-card:hover { - transform: none; - box-shadow: none; - } - - .chart-card-header { - flex-direction: column; - align-items: flex-start; - padding: 12px 14px; - gap: 10px; - } - - .coin-logo { - width: 30px; - height: 30px; - } - - .coin-title { - gap: 8px; - } - - .coin-title h3 { - font-size: 0.95rem; - } - - .coin-symbol { - font-size: 0.7rem; - } - - .price-info { - text-align: left; - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - } - - .current-price { - font-size: 1.1rem; - margin-bottom: 0; - } - - .price-change { - font-size: 0.7rem; - padding: 3px 6px; - } - - .chart-stats { - grid-template-columns: repeat(2, 1fr); - } - - .stat { - padding: 8px 10px; - } - - .stat-label { - font-size: 0.6rem; - letter-spacing: 0; - } - - .stat-value { - font-size: 0.75rem; - } - - .time-filters { - padding: 8px 10px; - gap: 4px; - justify-content: space-between; - } - - .time-btn { - padding: 5px 8px; - font-size: 0.65rem; - border-radius: 6px; - flex: 1; - text-align: center; - min-width: 0; - } - - .chart-container { - height: 200px; - padding: 10px; - } - - .chart-footer { - padding: 8px 10px; - flex-direction: row; - justify-content: space-between; - } - - .chart-info { - font-size: 0.65rem; - gap: 5px; - } - - .info-dot { - width: 6px; - height: 6px; - } - - .data-source { - font-size: 0.6rem; - } -} - -/* Extra Small Mobile (below 320px) */ -@media (max-width: 319px) { - .charts-page { - padding: 10px 8px 25px; - } - - .charts-header h1 { - font-size: 1.1rem; - } - - .charts-header p { - font-size: 0.8rem; - } - - .chart-card-header { - padding: 10px 12px; - } - - .coin-logo { - width: 26px; - height: 26px; - } - - .coin-title h3 { - font-size: 0.85rem; - } - - .current-price { - font-size: 1rem; - } - - .stat { - padding: 6px 8px; - } - - .stat-label { - font-size: 0.55rem; - } - - .stat-value { - font-size: 0.7rem; - } - - .time-btn { - padding: 4px 6px; - font-size: 0.6rem; - } - - .chart-container { - height: 180px; - padding: 8px; - } -} +/* Charts Page Styles */ + +.charts-page { + max-width: 1400px; + margin: 0 auto; + padding: 30px 20px 60px; + min-height: calc(100vh - 200px); +} + +.charts-header { + text-align: center; + margin-bottom: 40px; +} + +.charts-header h1 { + color: var(--foreground); + font-size: 2.2rem; + font-weight: 700; + margin: 0 0 10px 0; +} + +.charts-header p { + color: var(--muted-foreground); + font-size: 1.1rem; + margin: 0; +} + +/* Charts Grid - Desktop first, then responsive */ +.charts-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 25px; +} + +/* Chart Card */ +.chart-card { + background: var(--card); + border-radius: 16px; + border: 1px solid var(--border); + overflow: hidden; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.chart-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); +} + +/* Card Header */ +.chart-card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid var(--border); + flex-wrap: wrap; + gap: 12px; +} + +.coin-title { + display: flex; + align-items: center; + gap: 12px; +} + +.coin-logo { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; +} + +.coin-title h3 { + color: var(--foreground); + font-size: 1.25rem; + font-weight: 600; + margin: 0; +} + +.coin-symbol { + color: var(--muted-foreground); + font-size: 0.85rem; + display: block; +} + +.price-info { + text-align: right; +} + +.current-price { + color: var(--foreground); + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 4px; + line-height: 1.2; +} + +.price-change { + font-size: 0.9rem; + font-weight: 600; + padding: 4px 10px; + border-radius: 6px; + display: inline-block; +} + +.price-change.positive { + color: var(--color-up); + background: rgba(34, 197, 94, 0.15); +} + +.price-change.negative { + color: var(--color-down); + background: rgba(239, 68, 68, 0.15); +} + +/* Chart Stats */ +.chart-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1px; + background: var(--border); + border-bottom: 1px solid var(--border); +} + +.stat { + background: var(--card); + padding: 12px 8px; + text-align: center; +} + +.stat-label { + display: block; + color: var(--muted-foreground); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.3px; + margin-bottom: 4px; + white-space: nowrap; +} + +.stat-value { + display: block; + color: var(--foreground); + font-size: 0.85rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Time Filters */ +.time-filters { + display: flex; + gap: 6px; + padding: 12px 16px; + border-bottom: 1px solid var(--border); + flex-wrap: wrap; + justify-content: flex-start; +} + +.time-btn { + padding: 8px 14px; + background: transparent; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--muted-foreground); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.time-btn:hover { + border-color: var(--primary); + color: var(--primary); + background: rgba(245, 158, 11, 0.1); +} + +.time-btn.active { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-foreground); +} + +/* Chart Container */ +.chart-container { + position: relative; + height: 280px; + padding: 16px; +} + +.price-chart { + width: 100% !important; + height: 100% !important; +} + +/* Loading State */ +.chart-loading { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + color: var(--muted-foreground); + font-size: 0.9rem; + background: var(--card); +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.chart-loading .error { + color: var(--color-down); +} + +/* Chart Footer */ +.chart-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + background: rgba(23, 23, 23, 0.5); + border-top: 1px solid var(--border); +} + +.chart-info { + display: flex; + align-items: center; + gap: 8px; + color: var(--muted-foreground); + font-size: 0.75rem; +} + +.info-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-up); + flex-shrink: 0; +} + +.data-source { + color: var(--muted-foreground); + font-size: 0.7rem; +} + +/* ============================================ + RESPONSIVE BREAKPOINTS + ============================================ */ + +/* Large Desktop (1400px+) */ +@media (min-width: 1400px) { + .charts-grid { + grid-template-columns: repeat(2, 1fr); + gap: 30px; + } + + .chart-container { + height: 320px; + } +} + +/* Desktop / Laptop (1024px - 1399px) */ +@media (max-width: 1399px) and (min-width: 1024px) { + .charts-grid { + grid-template-columns: repeat(2, 1fr); + gap: 20px; + } + + .chart-card-header { + padding: 16px 20px; + } + + .chart-container { + height: 260px; + } +} + +/* Tablet Landscape (768px - 1023px) */ +@media (max-width: 1023px) and (min-width: 768px) { + .charts-page { + padding: 25px 15px 50px; + } + + .charts-header h1 { + font-size: 1.9rem; + } + + .charts-grid { + grid-template-columns: 1fr; + gap: 20px; + max-width: 700px; + margin: 0 auto; + } + + .chart-card-header { + padding: 18px 20px; + } + + .coin-title h3 { + font-size: 1.2rem; + } + + .current-price { + font-size: 1.4rem; + } + + .chart-stats { + grid-template-columns: repeat(4, 1fr); + } + + .stat { + padding: 12px 10px; + } + + .stat-label { + font-size: 0.7rem; + } + + .stat-value { + font-size: 0.85rem; + } + + .chart-container { + height: 280px; + padding: 16px 20px; + } +} + +/* Tablet Portrait (600px - 767px) */ +@media (max-width: 767px) and (min-width: 600px) { + .charts-page { + padding: 20px 15px 40px; + } + + .charts-header h1 { + font-size: 1.7rem; + } + + .charts-header p { + font-size: 1rem; + } + + .charts-grid { + grid-template-columns: 1fr; + gap: 18px; + } + + .chart-card-header { + padding: 16px 18px; + } + + .coin-logo { + width: 36px; + height: 36px; + } + + .coin-title h3 { + font-size: 1.1rem; + } + + .current-price { + font-size: 1.3rem; + } + + .chart-stats { + grid-template-columns: repeat(4, 1fr); + } + + .stat { + padding: 10px 6px; + } + + .stat-label { + font-size: 0.65rem; + } + + .stat-value { + font-size: 0.8rem; + } + + .time-filters { + padding: 10px 14px; + gap: 6px; + } + + .time-btn { + padding: 7px 12px; + font-size: 0.75rem; + } + + .chart-container { + height: 260px; + padding: 14px; + } +} + +/* Mobile Large (480px - 599px) */ +@media (max-width: 599px) and (min-width: 480px) { + .charts-page { + padding: 15px 12px 35px; + } + + .charts-header { + margin-bottom: 25px; + } + + .charts-header h1 { + font-size: 1.5rem; + } + + .charts-header p { + font-size: 0.9rem; + } + + .charts-grid { + grid-template-columns: 1fr; + gap: 15px; + } + + .chart-card { + border-radius: 12px; + } + + .chart-card:hover { + transform: none; + } + + .chart-card-header { + flex-direction: row; + justify-content: space-between; + padding: 14px 16px; + gap: 10px; + } + + .coin-logo { + width: 32px; + height: 32px; + } + + .coin-title { + gap: 10px; + } + + .coin-title h3 { + font-size: 1rem; + } + + .coin-symbol { + font-size: 0.75rem; + } + + .price-info { + text-align: right; + } + + .current-price { + font-size: 1.15rem; + } + + .price-change { + font-size: 0.75rem; + padding: 3px 8px; + } + + .chart-stats { + grid-template-columns: repeat(2, 1fr); + } + + .stat { + padding: 10px 12px; + } + + .stat-label { + font-size: 0.65rem; + } + + .stat-value { + font-size: 0.8rem; + } + + .time-filters { + padding: 10px 12px; + gap: 5px; + justify-content: space-between; + } + + .time-btn { + padding: 6px 10px; + font-size: 0.7rem; + flex: 1; + text-align: center; + } + + .chart-container { + height: 220px; + padding: 12px; + } + + .chart-footer { + padding: 8px 12px; + } + + .chart-info { + font-size: 0.7rem; + } + + .data-source { + font-size: 0.65rem; + } +} + +/* Mobile Small (320px - 479px) */ +@media (max-width: 479px) { + .charts-page { + padding: 12px 10px 30px; + } + + .charts-header { + margin-bottom: 20px; + } + + .charts-header h1 { + font-size: 1.3rem; + } + + .charts-header p { + font-size: 0.85rem; + } + + .charts-grid { + grid-template-columns: 1fr; + gap: 12px; + } + + .chart-card { + border-radius: 10px; + } + + .chart-card:hover { + transform: none; + box-shadow: none; + } + + .chart-card-header { + flex-direction: column; + align-items: flex-start; + padding: 12px 14px; + gap: 10px; + } + + .coin-logo { + width: 30px; + height: 30px; + } + + .coin-title { + gap: 8px; + } + + .coin-title h3 { + font-size: 0.95rem; + } + + .coin-symbol { + font-size: 0.7rem; + } + + .price-info { + text-align: left; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + } + + .current-price { + font-size: 1.1rem; + margin-bottom: 0; + } + + .price-change { + font-size: 0.7rem; + padding: 3px 6px; + } + + .chart-stats { + grid-template-columns: repeat(2, 1fr); + } + + .stat { + padding: 8px 10px; + } + + .stat-label { + font-size: 0.6rem; + letter-spacing: 0; + } + + .stat-value { + font-size: 0.75rem; + } + + .time-filters { + padding: 8px 10px; + gap: 4px; + justify-content: space-between; + } + + .time-btn { + padding: 5px 8px; + font-size: 0.65rem; + border-radius: 6px; + flex: 1; + text-align: center; + min-width: 0; + } + + .chart-container { + height: 200px; + padding: 10px; + } + + .chart-footer { + padding: 8px 10px; + flex-direction: row; + justify-content: space-between; + } + + .chart-info { + font-size: 0.65rem; + gap: 5px; + } + + .info-dot { + width: 6px; + height: 6px; + } + + .data-source { + font-size: 0.6rem; + } +} + +/* Extra Small Mobile (below 320px) */ +@media (max-width: 319px) { + .charts-page { + padding: 10px 8px 25px; + } + + .charts-header h1 { + font-size: 1.1rem; + } + + .charts-header p { + font-size: 0.8rem; + } + + .chart-card-header { + padding: 10px 12px; + } + + .coin-logo { + width: 26px; + height: 26px; + } + + .coin-title h3 { + font-size: 0.85rem; + } + + .current-price { + font-size: 1rem; + } + + .stat { + padding: 6px 8px; + } + + .stat-label { + font-size: 0.55rem; + } + + .stat-value { + font-size: 0.7rem; + } + + .time-btn { + padding: 4px 6px; + font-size: 0.6rem; + } + + .chart-container { + height: 180px; + padding: 8px; + } +} diff --git a/public/css/markets.css b/public/css/markets.css index f8d5326..03e1a9e 100644 --- a/public/css/markets.css +++ b/public/css/markets.css @@ -1,70 +1,265 @@ -body { - background-color: #23272f; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - color: #7c8d9e; - /* margin: 0; */ +.markets-container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; } -h1 { - font-size: 2rem; - font-weight: 700; - color: #b8afa8; - margin-bottom: 1.5rem; - text-align: center; - letter-spacing: 1px; +.markets-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + flex-wrap: wrap; + gap: 1rem; } -#market-table th, #market-table td { - background-color: #2c2f36; - color: #a4a19e; - text-transform: uppercase; - font-size: 0.85rem; - padding: 1rem 1.5rem; - text-align: left; - border-bottom: 1px solid #3a3f47; +.markets-title { + font-size: 2rem; + font-weight: 700; + color: var(--foreground); + margin: 0; } +.markets-search { + position: relative; + display: flex; + align-items: center; +} + +.search-icon { + position: absolute; + left: 0.75rem; + width: 16px; + height: 16px; + color: var(--muted-foreground); + pointer-events: none; +} + +.search-input { + padding: 0.5rem 1rem 0.5rem 2.25rem; + background: var(--input); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--foreground); + font-family: var(--font-sans); + font-size: 0.9rem; + outline: none; + width: 240px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.search-input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2); +} + +.search-input::placeholder { + color: var(--muted-foreground); +} + +.trending-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 2rem; +} + +.trending-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + display: flex; + align-items: center; + gap: 1rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.trending-card:hover { + border-color: var(--primary); + box-shadow: 0 0 16px rgba(245, 158, 11, 0.15); +} + +.trending-logo { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; +} + +.trending-info { + flex: 1; + min-width: 0; +} + +.trending-badge { + display: block; + font-size: 0.7rem; + font-weight: 600; + color: var(--primary); + background: rgba(245, 158, 11, 0.15); + padding: 0.15rem 0.5rem; + border-radius: 999px; + width: fit-content; + margin-bottom: 0.3rem; +} + +.trending-name { + font-weight: 600; + color: var(--foreground); + font-size: 0.95rem; +} + +.trending-symbol { + color: var(--muted-foreground); + font-size: 0.8rem; + text-transform: uppercase; +} + +.trending-price { + text-align: right; + flex-shrink: 0; +} + +.trending-price-value { + font-weight: 600; + color: var(--foreground); + font-size: 0.95rem; + display: block; + margin-bottom: 0.25rem; +} + +.market-loading { + display: flex; + justify-content: center; + align-items: center; + height: 200px; +} + +.market-spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(245, 158, 11, 0.2); + border-left-color: var(--primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.market-table-wrapper { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +.market-table { + width: 100%; + border-collapse: collapse; +} + +.market-table thead { + position: sticky; + top: 64px; + z-index: 10; +} + +.market-table th { + padding: 0.875rem 1rem; + background: var(--card); + color: var(--muted-foreground); + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.market-table td { + padding: 0.875rem 1rem; + color: var(--foreground); + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.market-table tbody tr:last-child td { + border-bottom: none; +} + +.market-table tbody tr:hover { + background: rgba(245, 158, 11, 0.04); +} + +.th-rank { + width: 50px; +} + +.td-rank { + color: var(--muted-foreground); + font-weight: 500; +} + +.th-right, .td-right { + text-align: right; +} + +.coin-info { + display: flex; + align-items: center; + gap: 0.75rem; +} .coin-info img { - box-shadow: 0 2px 8px rgba(253, 126, 20, 0.12); - border: 2px solid #fd7e14; + width: 28px; + height: 28px; + border-radius: 50%; +} + +.coin-name { + font-weight: 600; + color: var(--foreground); + font-size: 0.9rem; +} + +.coin-symbol { + color: var(--muted-foreground); + font-size: 0.75rem; + text-transform: uppercase; +} + +.badge-up { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 600; + background: rgba(34, 197, 94, 0.15); + color: var(--color-up); +} + +.badge-down { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 600; + background: rgba(239, 68, 68, 0.15); + color: var(--color-down); } -#market-table { +@media (max-width: 768px) { + .trending-cards { + grid-template-columns: 1fr; + } + .markets-header { + flex-direction: column; + align-items: flex-start; + } + .search-input { width: 100%; - max-width: 1200px; - margin: 0 auto; - border-collapse: collapse; - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18); - border-radius: 12px; - overflow: hidden; - background-color: #2c2f36; -} - -#market-data-body{ - font-size: 0.95rem; - color: #7c8d9e; - background-color: #23272f; - border-radius: 0 0 12px 12px; - overflow: hidden; - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18); -} - -.coin-info { display: flex; align-items: center; } -.coin-info img { width: 32px; height: 32px; margin-right: 1rem; border-radius: 50%; } -.coin-name { font-weight: 600; color: #fff; } -.coin-symbol { font-size: 0.8rem; color: #8b949e; text-transform: uppercase; } -.price-positive { color: #6be36f; } /* Highlight green */ -.price-negative { color: #ff6b6b; } /* A suitable red */ - -/* Loading Spinner */ -#loadingSpinner { display: flex; justify-content: center; align-items: center; height: 300px; } -.spinner { - border: 4px solid rgba(255, 255, 255, 0.2); - border-left-color: #fd7e14; - border-radius: 50%; - width: 50px; - height: 50px; - animation: spin 1s linear infinite; -} -@keyframes spin { to { transform: rotate(360deg); } } + } +} diff --git a/public/css/other.css b/public/css/other.css index 9a45b6c..756bf0a 100644 --- a/public/css/other.css +++ b/public/css/other.css @@ -1,40 +1,286 @@ -header .nav-link { - color: #C9D1D9; - transition: color 0.3s ease; - font-size: 1rem; - font-weight: 500; - padding: 0.5rem 1rem; +/* ── Navbar ── */ +.site-nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2rem; + height: 64px; + background: var(--sidebar); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 100; } -header .nav-link:hover { - color: #fd7e14; +.nav-logo { + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; } -header .nav-link.active { - color: #fd7e14; - font-weight: 700; +.nav-logo-img { + width: 36px; + height: 36px; + object-fit: contain; } -header { - background-color: #161b22; - color: #C9D1D9; - display: grid; - align-items: center; - padding: 100px; +.nav-logo-crypto { + font-size: 1.2rem; + font-weight: 700; + color: var(--primary); + letter-spacing: 1px; } +.nav-logo-pulse { + font-size: 1.2rem; + font-weight: 700; + color: var(--muted-foreground); + letter-spacing: 1px; +} + +.nav-links { + display: flex; + align-items: center; + gap: 0.25rem; + list-style: none; + margin: 0; + padding: 0; +} + +.nav-link { + color: var(--foreground); + text-decoration: none; + font-size: 0.95rem; + font-weight: 500; + padding: 0.4rem 0.75rem; + border-radius: var(--radius); + transition: color 0.2s ease; + position: relative; +} + +.nav-link:hover { + color: var(--primary); +} + +.nav-link.active { + color: var(--primary); +} + +.nav-link.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0.75rem; + right: 0.75rem; + height: 2px; + background: var(--primary); + border-radius: 1px; +} + +.nav-auth { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.btn-login { + padding: 0.4rem 1rem; + border: 1px solid var(--primary); + border-radius: var(--radius); + color: var(--primary); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + transition: background 0.2s ease; +} + +.btn-login:hover { + background: rgba(245, 158, 11, 0.1); + color: var(--primary); +} + +.btn-signup { + padding: 0.4rem 1rem; + background: var(--primary); + border: 1px solid var(--primary); + border-radius: var(--radius); + color: var(--primary-foreground); + text-decoration: none; + font-size: 0.9rem; + font-weight: 600; + transition: background 0.2s ease, border-color 0.2s ease; +} + +.btn-signup:hover { + background: #d97706; + border-color: #d97706; +} + +.nav-user-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.75rem; + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: 999px; + cursor: pointer; + color: var(--foreground); + font-family: var(--font-sans); +} + +.nav-avatar { + width: 28px; + height: 28px; + background: var(--primary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.8rem; + color: var(--primary-foreground); +} + +.nav-username { + color: var(--foreground); + font-size: 0.9rem; + font-weight: 500; +} + +.nav-dropdown { + background: var(--popover) !important; + border: 1px solid var(--border) !important; +} + +.nav-dropdown .dropdown-item { + color: var(--popover-foreground) !important; + font-size: 0.9rem; +} + +.nav-dropdown .dropdown-item:hover { + background: rgba(245, 158, 11, 0.1) !important; + color: var(--primary) !important; +} + +.nav-divider { + border-color: var(--border) !important; +} + +/* ── Footer ── */ .site-footer { - background: #0f3460; - padding: 20px 0; - text-align: center; - color: #fff; - font-size: 1rem; - width: 100%; - margin-top: 50px; -} - -.footer-content { - max-width: 1200px; - margin: 0 auto; - padding: 0 20px; + border-top: 1px solid var(--border); + background: var(--background); + padding: 3rem 2rem; + position: relative; +} + +.site-footer::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 33%; + height: 1px; + background: var(--foreground); + opacity: 0.2; + filter: blur(4px); +} + +.footer-inner { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 2fr; + gap: 2rem; +} + +.footer-brand { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.footer-logo { + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; +} + +.footer-logo-img { + width: 32px; + height: 32px; + object-fit: contain; +} + +.footer-copy { + color: var(--muted-foreground); + font-size: 0.85rem; + margin: 0; +} + +.footer-links-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 2rem; +} + +.footer-section h3 { + color: var(--foreground); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0 0 1rem 0; +} + +.footer-section ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.footer-section a { + color: var(--muted-foreground); + text-decoration: none; + font-size: 0.9rem; + display: inline-flex; + align-items: center; + gap: 0.35rem; + transition: color 0.3s ease; +} + +.footer-section a:hover { + color: var(--foreground); +} + +.footer-section svg { + width: 15px; + height: 15px; + flex-shrink: 0; +} + +/* ── Responsive ── */ +@media (max-width: 768px) { + .site-nav { + padding: 0 1rem; + } + + .nav-links { + display: none; + } + + .footer-inner { + grid-template-columns: 1fr; + } + + .footer-links-grid { + grid-template-columns: repeat(2, 1fr); + } } diff --git a/public/css/portfolio.css b/public/css/portfolio.css index 4a2361f..b057077 100644 --- a/public/css/portfolio.css +++ b/public/css/portfolio.css @@ -1,768 +1,768 @@ -/* Portfolio Dashboard Styles */ - -.portfolio-container { - max-width: 1400px; - margin: 0 auto; - padding: 30px 20px; -} - -.portfolio-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 30px; - flex-wrap: wrap; - gap: 20px; -} - -.portfolio-header h1 { - color: #ffffff; - font-size: 2rem; - margin: 0; -} - -.portfolio-header p { - color: #8b949e; - margin: 5px 0 0 0; -} - -.btn-add-holding { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 24px; - background: linear-gradient(135deg, #fd7e14 0%, #e55a00 100%); - border: none; - border-radius: 10px; - color: white; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; -} - -.btn-add-holding:hover { - background: linear-gradient(135deg, #e55a00 0%, #cc4a00 100%); - transform: translateY(-2px); - box-shadow: 0 10px 30px rgba(253, 126, 20, 0.3); -} - -/* Summary Cards */ -.summary-cards { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 20px; - margin-bottom: 40px; -} - -.summary-card { - background: #1e2a3a; - border-radius: 15px; - padding: 25px; - border: 1px solid #30363d; -} - -.summary-label { - color: #8b949e; - font-size: 0.9rem; - margin-bottom: 10px; -} - -.summary-value { - color: #ffffff; - font-size: 1.8rem; - font-weight: 700; -} - -.summary-percent { - font-size: 1rem; - font-weight: 500; -} - -.summary-card.profit .summary-value { - color: #2ecc71; -} - -.summary-card.loss .summary-value { - color: #e74c3c; -} - -/* Holdings Section */ -.holdings-section { - background: #1e2a3a; - border-radius: 15px; - padding: 25px; - border: 1px solid #30363d; -} - -.holdings-section h2 { - color: #ffffff; - font-size: 1.3rem; - margin: 0 0 20px 0; -} - -/* Empty State */ -.empty-state { - text-align: center; - padding: 60px 20px; - color: #8b949e; -} - -.empty-state svg { - margin-bottom: 20px; - opacity: 0.5; -} - -.empty-state h3 { - color: #c9d1d9; - margin-bottom: 10px; -} - -.empty-state p { - margin-bottom: 25px; -} - -/* Holdings Table */ -.table-responsive { - overflow-x: auto; -} - -.holdings-table { - width: 100%; - border-collapse: collapse; -} - -.holdings-table th { - text-align: left; - padding: 15px; - color: #8b949e; - font-weight: 500; - font-size: 0.85rem; - text-transform: uppercase; - border-bottom: 1px solid #30363d; -} - -.holdings-table td { - padding: 15px; - color: #c9d1d9; - border-bottom: 1px solid #30363d; -} - -.holdings-table tbody tr:hover { - background: rgba(253, 126, 20, 0.05); -} - -.coin-info { - display: flex; - align-items: center; - gap: 12px; -} - -.coin-icon { - width: 40px; - height: 40px; - border-radius: 50%; -} - -.coin-name { - font-weight: 600; - color: #ffffff; -} - -.coin-symbol { - color: #8b949e; - font-size: 0.85rem; -} - -.price-change { - font-size: 0.8rem; - margin-top: 2px; -} - -.price-change.positive { - color: #2ecc71; -} - -.price-change.negative { - color: #e74c3c; -} - -.profit { - color: #2ecc71 !important; - font-weight: 600; -} - -.loss { - color: #e74c3c !important; - font-weight: 600; -} - -/* Action Buttons */ -.action-buttons { - display: flex; - gap: 8px; -} - -.btn-action { - width: 36px; - height: 36px; - border: none; - border-radius: 8px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; -} - -.btn-edit { - background: rgba(253, 126, 20, 0.1); - color: #fd7e14; -} - -.btn-edit:hover { - background: rgba(253, 126, 20, 0.2); -} - -.btn-delete { - background: rgba(231, 76, 60, 0.1); - color: #e74c3c; -} - -.btn-delete:hover { - background: rgba(231, 76, 60, 0.2); -} - -/* Modal Styles */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 20px; -} - -.modal-content { - background: #1e2a3a; - border-radius: 20px; - width: 100%; - max-width: 500px; - max-height: 90vh; - overflow-y: auto; - border: 1px solid #30363d; -} - -.modal-small { - max-width: 400px; - text-align: center; -} - -.modal-small p { - color: #c9d1d9; - margin: 0 0 25px 0; - padding: 0 20px; -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 25px; - border-bottom: 1px solid #30363d; -} - -.modal-header h2 { - color: #ffffff; - font-size: 1.3rem; - margin: 0; -} - -.modal-close { - background: none; - border: none; - color: #8b949e; - font-size: 1.5rem; - cursor: pointer; - padding: 0; - line-height: 1; -} - -.modal-close:hover { - color: #ffffff; -} - -#holdingForm { - padding: 25px; -} - -.form-group { - margin-bottom: 20px; -} - -.form-group label { - display: block; - color: #c9d1d9; - font-size: 0.9rem; - margin-bottom: 8px; -} - -.form-control { - width: 100%; - padding: 12px 15px; - background: #0d1b2a; - border: 1px solid #30363d; - border-radius: 10px; - color: #ffffff; - font-size: 1rem; -} - -.form-control:focus { - outline: none; - border-color: #fd7e14; - box-shadow: 0 0 0 3px rgba(253, 126, 20, 0.2); -} - -.form-control::placeholder { - color: #6e7681; -} - -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 15px; -} - -/* Search Styles */ -.search-wrapper { - position: relative; -} - -.search-results { - position: absolute; - top: 100%; - left: 0; - right: 0; - background: #0d1b2a; - border: 1px solid #30363d; - border-radius: 10px; - margin-top: 5px; - max-height: 250px; - overflow-y: auto; - z-index: 10; - display: none; -} - -.search-results.show { - display: block; -} - -.search-result-item { - display: flex; - align-items: center; - gap: 12px; - padding: 12px 15px; - cursor: pointer; - transition: background 0.2s ease; -} - -.search-result-item:hover { - background: rgba(253, 126, 20, 0.1); -} - -.search-result-item img { - width: 32px; - height: 32px; - border-radius: 50%; -} - -.search-result-item .name { - color: #ffffff; - font-weight: 500; -} - -.search-result-item .symbol { - color: #8b949e; - font-size: 0.85rem; -} - -.search-result-item .price { - margin-left: auto; - color: #2ecc71; - font-weight: 500; -} - -/* Selected Coin */ -.selected-coin { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 15px; - background: rgba(253, 126, 20, 0.1); - border: 1px solid rgba(253, 126, 20, 0.3); - border-radius: 10px; - margin-top: 10px; -} - -.selected-coin img { - width: 28px; - height: 28px; - border-radius: 50%; -} - -.selected-coin span { - color: #ffffff; - font-weight: 500; -} - -.btn-clear { - margin-left: auto; - background: none; - border: none; - color: #8b949e; - font-size: 1.2rem; - cursor: pointer; - padding: 0 5px; -} - -.btn-clear:hover { - color: #e74c3c; -} - -/* Price Input */ -.price-input-wrapper { - display: flex; - gap: 10px; -} - -.price-input-wrapper .form-control { - flex: 1; -} - -.btn-current-price { - padding: 0 15px; - background: rgba(253, 126, 20, 0.1); - border: 1px solid rgba(253, 126, 20, 0.3); - border-radius: 10px; - color: #fd7e14; - font-size: 0.85rem; - cursor: pointer; - white-space: nowrap; -} - -.btn-current-price:hover { - background: rgba(253, 126, 20, 0.2); -} - -/* Modal Actions */ -.modal-actions { - display: flex; - gap: 15px; - margin-top: 25px; -} - -.btn-cancel { - flex: 1; - padding: 12px; - background: transparent; - border: 1px solid #30363d; - border-radius: 10px; - color: #c9d1d9; - font-size: 1rem; - cursor: pointer; -} - -.btn-cancel:hover { - background: rgba(255, 255, 255, 0.05); -} - -.btn-save { - flex: 1; - padding: 12px; - background: linear-gradient(135deg, #fd7e14 0%, #e55a00 100%); - border: none; - border-radius: 10px; - color: white; - font-size: 1rem; - font-weight: 600; - cursor: pointer; -} - -.btn-save:hover { - background: linear-gradient(135deg, #e55a00 0%, #cc4a00 100%); -} - -.btn-delete-confirm { - flex: 1; - padding: 12px; - background: #e74c3c; - border: none; - border-radius: 10px; - color: white; - font-size: 1rem; - font-weight: 600; - cursor: pointer; -} - -.btn-delete-confirm:hover { - background: #c0392b; -} - -/* Toast Notification */ -.toast { - position: fixed; - bottom: 30px; - right: 30px; - padding: 15px 25px; - background: #1e2a3a; - border-radius: 10px; - border: 1px solid #30363d; - color: #ffffff; - font-weight: 500; - z-index: 1001; - transform: translateY(100px); - opacity: 0; - transition: all 0.3s ease; -} - -.toast.show { - transform: translateY(0); - opacity: 1; -} - -.toast.success { - border-color: #2ecc71; - background: rgba(46, 204, 113, 0.1); -} - -.toast.error { - border-color: #e74c3c; - background: rgba(231, 76, 60, 0.1); -} - -/* Analytics Section */ -.analytics-section { - margin-top: 40px; -} - -.analytics-section h2 { - color: #ffffff; - font-size: 1.5rem; - margin-bottom: 25px; -} - -.analytics-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - gap: 25px; - margin-bottom: 25px; -} - -.analytics-card { - background: #1e2a3a; - border-radius: 15px; - padding: 25px; - border: 1px solid #30363d; -} - -.analytics-card h3 { - color: #ffffff; - font-size: 1.1rem; - margin: 0 0 20px 0; -} - -.chart-container { - position: relative; - height: 280px; -} - -/* Time Filter Buttons */ -.time-filters { - display: flex; - gap: 8px; - margin-bottom: 15px; -} - -.time-btn { - padding: 6px 16px; - background: transparent; - border: 1px solid #30363d; - border-radius: 20px; - color: #8b949e; - font-size: 0.85rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.time-btn:hover { - border-color: #fd7e14; - color: #fd7e14; -} - -.time-btn.active { - background: rgba(253, 126, 20, 0.2); - border-color: #fd7e14; - color: #fd7e14; -} - -/* Best/Worst Performers */ -.performers-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 20px; - margin-bottom: 25px; -} - -.performer-card { - background: #1e2a3a; - border-radius: 15px; - padding: 20px; - border: 1px solid #30363d; -} - -.performer-card.best { - border-color: rgba(46, 204, 113, 0.3); - background: linear-gradient(135deg, rgba(46, 204, 113, 0.1) 0%, #1e2a3a 100%); -} - -.performer-card.worst { - border-color: rgba(231, 76, 60, 0.3); - background: linear-gradient(135deg, rgba(231, 76, 60, 0.1) 0%, #1e2a3a 100%); -} - -.performer-label { - color: #8b949e; - font-size: 0.85rem; - margin-bottom: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.performer-info { - display: flex; - align-items: center; - gap: 15px; -} - -.performer-icon { - width: 48px; - height: 48px; - border-radius: 50%; -} - -.performer-name { - color: #ffffff; - font-weight: 600; - margin-bottom: 4px; -} - -.performer-change { - font-size: 1.2rem; - font-weight: 700; -} - -.performer-change.positive { - color: #2ecc71; -} - -.performer-change.negative { - color: #e74c3c; -} - -/* Performance Metrics */ -.metrics-section { - background: #1e2a3a; - border-radius: 15px; - padding: 25px; - border: 1px solid #30363d; -} - -.metrics-section h3 { - color: #ffffff; - font-size: 1.1rem; - margin: 0 0 20px 0; -} - -.metrics-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 15px; -} - -.metric-card { - text-align: center; - padding: 15px; - background: rgba(13, 27, 42, 0.5); - border-radius: 10px; -} - -.metric-label { - color: #8b949e; - font-size: 0.85rem; - margin-bottom: 8px; -} - -.metric-value { - font-size: 1.4rem; - font-weight: 700; -} - -.metric-value.positive { - color: #2ecc71; -} - -.metric-value.negative { - color: #e74c3c; -} - -.metric-value.neutral { - color: #ffffff; -} - -/* Responsive */ -@media (max-width: 768px) { - .portfolio-header { - flex-direction: column; - align-items: flex-start; - } - - .summary-cards { - grid-template-columns: 1fr; - } - - .form-row { - grid-template-columns: 1fr; - } - - .holdings-table th, - .holdings-table td { - padding: 10px; - font-size: 0.9rem; - } - - .coin-icon { - width: 32px; - height: 32px; - } - - .analytics-grid { - grid-template-columns: 1fr; - } - - .performers-grid { - grid-template-columns: 1fr; - } - - .metrics-grid { - grid-template-columns: repeat(2, 1fr); - } - - .chart-container { - height: 250px; - } -} +/* Portfolio Dashboard Styles */ + +.portfolio-container { + max-width: 1400px; + margin: 0 auto; + padding: 30px 20px; +} + +.portfolio-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + flex-wrap: wrap; + gap: 20px; +} + +.portfolio-header h1 { + color: var(--foreground); + font-size: 2rem; + margin: 0; +} + +.portfolio-header p { + color: var(--muted-foreground); + margin: 5px 0 0 0; +} + +.btn-add-holding { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + background: linear-gradient(135deg, var(--primary) 0%, #d97706 100%); + border: none; + border-radius: 10px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-add-holding:hover { + background: linear-gradient(135deg, #d97706 0%, #b45309 100%); + transform: translateY(-2px); + box-shadow: 0 10px 30px rgba(245, 158, 11, 0.3); +} + +/* Summary Cards */ +.summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; + margin-bottom: 40px; +} + +.summary-card { + background: var(--card); + border-radius: 15px; + padding: 25px; + border: 1px solid var(--border); +} + +.summary-label { + color: var(--muted-foreground); + font-size: 0.9rem; + margin-bottom: 10px; +} + +.summary-value { + color: var(--foreground); + font-size: 1.8rem; + font-weight: 700; +} + +.summary-percent { + font-size: 1rem; + font-weight: 500; +} + +.summary-card.profit .summary-value { + color: var(--color-up); +} + +.summary-card.loss .summary-value { + color: var(--color-down); +} + +/* Holdings Section */ +.holdings-section { + background: var(--card); + border-radius: 15px; + padding: 25px; + border: 1px solid var(--border); +} + +.holdings-section h2 { + color: var(--foreground); + font-size: 1.3rem; + margin: 0 0 20px 0; +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 60px 20px; + color: var(--muted-foreground); +} + +.empty-state svg { + margin-bottom: 20px; + opacity: 0.5; +} + +.empty-state h3 { + color: var(--card-foreground); + margin-bottom: 10px; +} + +.empty-state p { + margin-bottom: 25px; +} + +/* Holdings Table */ +.table-responsive { + overflow-x: auto; +} + +.holdings-table { + width: 100%; + border-collapse: collapse; +} + +.holdings-table th { + text-align: left; + padding: 15px; + color: var(--muted-foreground); + font-weight: 500; + font-size: 0.85rem; + text-transform: uppercase; + border-bottom: 1px solid var(--border); +} + +.holdings-table td { + padding: 15px; + color: var(--card-foreground); + border-bottom: 1px solid var(--border); +} + +.holdings-table tbody tr:hover { + background: rgba(245, 158, 11, 0.05); +} + +.coin-info { + display: flex; + align-items: center; + gap: 12px; +} + +.coin-icon { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.coin-name { + font-weight: 600; + color: var(--foreground); +} + +.coin-symbol { + color: var(--muted-foreground); + font-size: 0.85rem; +} + +.price-change { + font-size: 0.8rem; + margin-top: 2px; +} + +.price-change.positive { + color: var(--color-up); +} + +.price-change.negative { + color: var(--color-down); +} + +.profit { + color: var(--color-up) !important; + font-weight: 600; +} + +.loss { + color: var(--color-down) !important; + font-weight: 600; +} + +/* Action Buttons */ +.action-buttons { + display: flex; + gap: 8px; +} + +.btn-action { + width: 36px; + height: 36px; + border: none; + border-radius: 8px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.btn-edit { + background: rgba(245, 158, 11, 0.1); + color: var(--primary); +} + +.btn-edit:hover { + background: rgba(245, 158, 11, 0.2); +} + +.btn-delete { + background: rgba(239, 68, 68, 0.1); + color: var(--color-down); +} + +.btn-delete:hover { + background: rgba(239, 68, 68, 0.2); +} + +/* Modal Styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; +} + +.modal-content { + background: var(--card); + border-radius: 20px; + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + border: 1px solid var(--border); +} + +.modal-small { + max-width: 400px; + text-align: center; +} + +.modal-small p { + color: var(--card-foreground); + margin: 0 0 25px 0; + padding: 0 20px; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 25px; + border-bottom: 1px solid var(--border); +} + +.modal-header h2 { + color: var(--foreground); + font-size: 1.3rem; + margin: 0; +} + +.modal-close { + background: none; + border: none; + color: var(--muted-foreground); + font-size: 1.5rem; + cursor: pointer; + padding: 0; + line-height: 1; +} + +.modal-close:hover { + color: var(--foreground); +} + +#holdingForm { + padding: 25px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + color: var(--card-foreground); + font-size: 0.9rem; + margin-bottom: 8px; +} + +.form-control { + width: 100%; + padding: 12px 15px; + background: var(--input); + border: 1px solid var(--border); + border-radius: 10px; + color: var(--foreground); + font-size: 1rem; +} + +.form-control:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2); +} + +.form-control::placeholder { + color: var(--muted-foreground); +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +/* Search Styles */ +.search-wrapper { + position: relative; +} + +.search-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--input); + border: 1px solid var(--border); + border-radius: 10px; + margin-top: 5px; + max-height: 250px; + overflow-y: auto; + z-index: 10; + display: none; +} + +.search-results.show { + display: block; +} + +.search-result-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 15px; + cursor: pointer; + transition: background 0.2s ease; +} + +.search-result-item:hover { + background: rgba(245, 158, 11, 0.1); +} + +.search-result-item img { + width: 32px; + height: 32px; + border-radius: 50%; +} + +.search-result-item .name { + color: var(--foreground); + font-weight: 500; +} + +.search-result-item .symbol { + color: var(--muted-foreground); + font-size: 0.85rem; +} + +.search-result-item .price { + margin-left: auto; + color: var(--color-up); + font-weight: 500; +} + +/* Selected Coin */ +.selected-coin { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 15px; + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: 10px; + margin-top: 10px; +} + +.selected-coin img { + width: 28px; + height: 28px; + border-radius: 50%; +} + +.selected-coin span { + color: var(--foreground); + font-weight: 500; +} + +.btn-clear { + margin-left: auto; + background: none; + border: none; + color: var(--muted-foreground); + font-size: 1.2rem; + cursor: pointer; + padding: 0 5px; +} + +.btn-clear:hover { + color: var(--color-down); +} + +/* Price Input */ +.price-input-wrapper { + display: flex; + gap: 10px; +} + +.price-input-wrapper .form-control { + flex: 1; +} + +.btn-current-price { + padding: 0 15px; + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: 10px; + color: var(--primary); + font-size: 0.85rem; + cursor: pointer; + white-space: nowrap; +} + +.btn-current-price:hover { + background: rgba(245, 158, 11, 0.2); +} + +/* Modal Actions */ +.modal-actions { + display: flex; + gap: 15px; + margin-top: 25px; +} + +.btn-cancel { + flex: 1; + padding: 12px; + background: transparent; + border: 1px solid var(--border); + border-radius: 10px; + color: var(--card-foreground); + font-size: 1rem; + cursor: pointer; +} + +.btn-cancel:hover { + background: rgba(255, 255, 255, 0.05); +} + +.btn-save { + flex: 1; + padding: 12px; + background: linear-gradient(135deg, var(--primary) 0%, #d97706 100%); + border: none; + border-radius: 10px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; +} + +.btn-save:hover { + background: linear-gradient(135deg, #d97706 0%, #b45309 100%); +} + +.btn-delete-confirm { + flex: 1; + padding: 12px; + background: var(--color-down); + border: none; + border-radius: 10px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; +} + +.btn-delete-confirm:hover { + background: #c0392b; +} + +/* Toast Notification */ +.toast { + position: fixed; + bottom: 30px; + right: 30px; + padding: 15px 25px; + background: var(--card); + border-radius: 10px; + border: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; + z-index: 1001; + transform: translateY(100px); + opacity: 0; + transition: all 0.3s ease; +} + +.toast.show { + transform: translateY(0); + opacity: 1; +} + +.toast.success { + border-color: var(--color-up); + background: rgba(34, 197, 94, 0.1); +} + +.toast.error { + border-color: var(--color-down); + background: rgba(239, 68, 68, 0.1); +} + +/* Analytics Section */ +.analytics-section { + margin-top: 40px; +} + +.analytics-section h2 { + color: var(--foreground); + font-size: 1.5rem; + margin-bottom: 25px; +} + +.analytics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 25px; + margin-bottom: 25px; +} + +.analytics-card { + background: var(--card); + border-radius: 15px; + padding: 25px; + border: 1px solid var(--border); +} + +.analytics-card h3 { + color: var(--foreground); + font-size: 1.1rem; + margin: 0 0 20px 0; +} + +.chart-container { + position: relative; + height: 280px; +} + +/* Time Filter Buttons */ +.time-filters { + display: flex; + gap: 8px; + margin-bottom: 15px; +} + +.time-btn { + padding: 6px 16px; + background: transparent; + border: 1px solid var(--border); + border-radius: 20px; + color: var(--muted-foreground); + font-size: 0.85rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.time-btn:hover { + border-color: var(--primary); + color: var(--primary); +} + +.time-btn.active { + background: rgba(245, 158, 11, 0.2); + border-color: var(--primary); + color: var(--primary); +} + +/* Best/Worst Performers */ +.performers-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; + margin-bottom: 25px; +} + +.performer-card { + background: var(--card); + border-radius: 15px; + padding: 20px; + border: 1px solid var(--border); +} + +.performer-card.best { + border-color: rgba(34, 197, 94, 0.3); + background: linear-gradient(135deg, rgba(34, 197, 94, 0.1) 0%, var(--card) 100%); +} + +.performer-card.worst { + border-color: rgba(239, 68, 68, 0.3); + background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, var(--card) 100%); +} + +.performer-label { + color: var(--muted-foreground); + font-size: 0.85rem; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.performer-info { + display: flex; + align-items: center; + gap: 15px; +} + +.performer-icon { + width: 48px; + height: 48px; + border-radius: 50%; +} + +.performer-name { + color: var(--foreground); + font-weight: 600; + margin-bottom: 4px; +} + +.performer-change { + font-size: 1.2rem; + font-weight: 700; +} + +.performer-change.positive { + color: var(--color-up); +} + +.performer-change.negative { + color: var(--color-down); +} + +/* Performance Metrics */ +.metrics-section { + background: var(--card); + border-radius: 15px; + padding: 25px; + border: 1px solid var(--border); +} + +.metrics-section h3 { + color: var(--foreground); + font-size: 1.1rem; + margin: 0 0 20px 0; +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; +} + +.metric-card { + text-align: center; + padding: 15px; + background: rgba(23, 23, 23, 0.5); + border-radius: 10px; +} + +.metric-label { + color: var(--muted-foreground); + font-size: 0.85rem; + margin-bottom: 8px; +} + +.metric-value { + font-size: 1.4rem; + font-weight: 700; +} + +.metric-value.positive { + color: var(--color-up); +} + +.metric-value.negative { + color: var(--color-down); +} + +.metric-value.neutral { + color: var(--foreground); +} + +/* Responsive */ +@media (max-width: 768px) { + .portfolio-header { + flex-direction: column; + align-items: flex-start; + } + + .summary-cards { + grid-template-columns: 1fr; + } + + .form-row { + grid-template-columns: 1fr; + } + + .holdings-table th, + .holdings-table td { + padding: 10px; + font-size: 0.9rem; + } + + .coin-icon { + width: 32px; + height: 32px; + } + + .analytics-grid { + grid-template-columns: 1fr; + } + + .performers-grid { + grid-template-columns: 1fr; + } + + .metrics-grid { + grid-template-columns: repeat(2, 1fr); + } + + .chart-container { + height: 250px; + } +} diff --git a/public/css/styles.css b/public/css/styles.css index 588f177..a055311 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -1,253 +1,379 @@ -@import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@700&display=swap'); +/* ── Hero ── */ +.hero-section { + padding: 5rem 2rem; + background: var(--background); + background-image: radial-gradient(ellipse 60% 50% at 70% 50%, rgba(245, 158, 11, 0.12), transparent); + overflow: hidden; +} -html, body { - margin: 0; - padding: 0; - width: 100%; - overflow-x: hidden; +.hero-inner { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 55% 45%; + align-items: center; + gap: 2rem; } -body { - background-color:#323539; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - color: #C9D1D9; +.hero-heading { + font-size: clamp(2.5rem, 5vw, 4rem); + font-weight: 700; + line-height: 1.1; + margin: 0 0 1.5rem 0; } -h1 { - color: #FFFFFF; +.hero-crypto { + color: var(--primary); + display: inline-block; } -p { - font-size: 18px; - color: #C9D1D9; +.hero-pulse { + color: var(--muted-foreground); + display: inline-block; } -.main-intro { - color: #FFFFFF; - font-size: 2.5rem; - margin-top: 20px; - /* display: inline-flex; */ - align-items: left; - justify-content: left; + +.hero-sub { + color: var(--muted-foreground); + font-size: 1.15rem; + line-height: 1.6; + margin: 0 0 2rem 0; } -.sub-intro { - color: #C9D1D9; - font-size: 1.7rem; - margin-top: 10px; +.hero-actions { + display: flex; + gap: 1rem; + flex-wrap: wrap; } -.custom-container { - display: grid; - grid-template-columns: 1fr 1fr; - align-items: center; - max-width: 1200px; - margin: 50px auto; - gap: 2rem; - padding: 20px; +.btn-primary-filled { + padding: 0.65rem 1.5rem; + background: var(--primary); + color: var(--primary-foreground); + border: none; + border-radius: var(--radius); + font-size: 1rem; + font-weight: 600; + text-decoration: none; + display: inline-block; + transition: background 0.2s ease, transform 0.2s ease; } -.text-content { - text-align: left; +.btn-primary-filled:hover { + background: #d97706; + transform: translateY(-2px); + color: var(--primary-foreground); } -.image-content { - display: grid; - width: 100%; - align-items: center; - grid-template-columns: 1fr 1fr; +.btn-primary-outline { + padding: 0.65rem 1.5rem; + background: transparent; + color: var(--primary); + border: 1px solid var(--primary); + border-radius: var(--radius); + font-size: 1rem; + font-weight: 600; + text-decoration: none; + display: inline-block; + transition: background 0.2s ease; +} + +.btn-primary-outline:hover { + background: rgba(245, 158, 11, 0.1); + color: var(--primary); +} + +.hero-lottie { + display: flex; justify-content: center; - margin-left: 100px; + align-items: center; } -.bitcoin-img { - max-width: 220px; - max-height: fit-content; - background: #fff; - padding: 2rem; - border-radius: 50px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +/* ── Stats ── */ +.stats-section { + padding: 4rem 2rem; + background: var(--muted); +} + +.stats-inner { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(6, 1fr); gap: 1rem; } -.highlight-orange { - color: #fd7e14; +.stat-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem 1rem; + text-align: center; + display: flex; + flex-direction: column; + gap: 0.5rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.stat-card:hover { + border-color: var(--primary); + box-shadow: 0 0 16px rgba(245, 158, 11, 0.15); } -.highlight-light { - color: #e9912d; +.stat-number { + font-size: 1.8rem; + font-weight: 700; + color: var(--primary); + display: block; } -.highlight-green { - color: #6be36f; +.stat-label { + font-size: 0.75rem; + color: var(--muted-foreground); + text-transform: uppercase; + letter-spacing: 0.05em; + display: block; } -.btn1{ - background-color: #fd7e14; - border-color: #fd7e14; - color: #212529; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-size: 1rem; - font-weight: 500; +/* ── Features ── */ +.features-section { + padding: 5rem 2rem; + background: var(--background); } -.text1{ - display: grid; - /* grid-template-columns: 1fr 1fr; */ - align-items: center; - justify-content: center; +.features-inner { max-width: 1200px; - margin: 50px auto; - /* gap: 2rem; */ - /* padding: 20px; */ - + margin: 0 auto; + display: grid; + grid-template-columns: 45% 55%; + align-items: center; + gap: 3rem; } -.sub-intro2 { - color: #C9D1D9; - font-size: 1.2rem; - margin-top: 10px; +.features-heading { + font-size: 2rem; + font-weight: 700; + color: var(--foreground); + margin: 0 0 2rem 0; } -.ethereum-img { - max-width: 220px; - max-height: fit-content; - background: #fff; - padding: 2rem; - border-radius: 50px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +.bullet-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; gap: 1rem; } -.info-content { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - grid-template-rows: 1fr 1fr; - gap: 2rem; - margin-top: 20px; +.bullet-list li { + color: var(--foreground); + font-size: 1rem; + line-height: 1.5; + display: flex; + align-items: flex-start; + gap: 0.75rem; } -.coin-icon { - width: 80px; - animation: float 2s ease-in-out infinite; +.bullet-icon { + color: var(--primary); + flex-shrink: 0; + font-size: 1rem; + margin-top: 2px; } -@keyframes float { - 0% { transform: translateY(0); } - 50% { transform: translateY(-10px); } - 100% { transform: translateY(0); } +/* ── Testimonials ── */ +.testimonials-section { + padding: 5rem 0; + background: var(--muted); + overflow: hidden; } -.blockchain-ani { - display: grid; - grid-template-rows: auto; - grid-template-columns: 1fr 1fr; - grid-template-areas: "imp-info ani1" - "imp-info2 ani1"; - align-items: center; - justify-content: center; - padding: 20px; - margin-left: 16%; - color: #C9D1D9; - /* border-left: 2px solid #FF6B00; */ +.section-heading { + text-align: center; + font-size: 2rem; + font-weight: 700; + color: var(--foreground); + margin: 0 0 3rem 0; + padding: 0 2rem; } -.imp-info { - font-size: 1rem; - color: #C9D1D9; - margin-top: 10px; - border-left: #6be36f 2px solid; - padding-left: 10px; - grid-area: imp-info; +.marquee-wrapper { + display: flex; + flex-direction: column; + gap: 1rem; } -.ani1{ - display:flex; - justify-content: center; - align-items: center; - grid-area: ani1; +.marquee-wrapper:hover .marquee-track { + animation-play-state: paused; +} + +.marquee-track { + display: flex; + gap: 1rem; + width: max-content; + animation: marquee-left 40s linear infinite; +} +.marquee-track.reverse { + animation: marquee-right 40s linear infinite; } -.imp-info2 { - font-size: 1rem; - color: #C9D1D9; - margin-top: 10px; - border-left: #6be36f 2px solid; - padding-left: 10px; - grid-area: imp-info2; +@keyframes marquee-left { + from { transform: translateX(0); } + to { transform: translateX(-50%); } } -.container2{ - color: #C9D1D9; - margin-top: 40px; - padding: 20px; +@keyframes marquee-right { + from { transform: translateX(-50%); } + to { transform: translateX(0); } } -.ani2{ - display:flex; - justify-content: center; +.testimonial-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem; + width: 280px; + flex-shrink: 0; +} + +.testimonial-header { + display: flex; align-items: center; - /* grid-area: ani2; */ - margin-bottom: 25%; + gap: 0.75rem; + margin-bottom: 0.75rem; } -.slot1{ - display: grid ; - grid-template-columns: 1fr 1fr; +.testimonial-avatar { + width: 36px; + height: 36px; + background: var(--primary); + color: var(--primary-foreground); + border-radius: 50%; + display: flex; align-items: center; justify-content: center; - padding: 30px; + font-weight: 700; + font-size: 0.9rem; + flex-shrink: 0; } -.imp-info3 { - font-size: 1rem; - color: #C9D1D9; - border-left: #6be36f 2px solid; - padding-left: 10px; - margin-bottom: 150px; +.testimonial-name { + font-weight: 600; + color: var(--foreground); + font-size: 0.9rem; } -.card { - background-color: #0e213a; - border-radius: 40px; - padding: 20px; - color: #C9D1D9; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +.testimonial-role { + font-size: 0.8rem; + color: var(--muted-foreground); } -.card-img-top { - border-radius: 40px 40px 40px 40px; - max-width: 100%; - height: auto; +.testimonial-stars { + color: var(--primary); + font-size: 0.85rem; + margin-bottom: 0.75rem; } -.card:hover { - transform: scale(1.05); - border: 2px solid #fd7e14; - box-shadow: 0 0 15px rgba(253, 126, 20, 0.5); - transition: all 0.3s ease-in-out; -} +.testimonial-quote { + color: var(--muted-foreground); + font-size: 0.875rem; + line-height: 1.5; + margin: 0; +} -.cryptoHe{ - color: #FFFFFF; - font-size: 2.5rem; - margin-top: 20px; - text-align: center; - margin-bottom: 50px; +/* ── Why Us ── */ +.why-section { + padding: 5rem 2rem; + background: var(--background); } -.container{ - margin-bottom: 100px; +.why-inner { + max-width: 900px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; } -.ForGraphs { - display: flex; - justify-content: center; - align-items: center; - margin-top: 100px; - margin-bottom: 100px; +.why-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2rem; +} + +.why-us { + border-color: var(--primary); + box-shadow: 0 0 24px rgba(245, 158, 11, 0.15); } +.why-card-title { + font-size: 1rem; + font-weight: 700; + color: var(--muted-foreground); + margin: 0 0 1.5rem 0; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.why-us-title { + color: var(--primary); +} + +.why-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.why-list li { + color: var(--foreground); + font-size: 0.95rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.why-x { + color: var(--color-down); + font-weight: 700; + flex-shrink: 0; +} + +.why-check { + color: var(--color-up); + font-weight: 700; + flex-shrink: 0; +} + +/* ── Responsive ── */ +@media (max-width: 1100px) { + .stats-inner { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 768px) { + .hero-inner { + grid-template-columns: 1fr; + } + .hero-lottie { + display: none; + } + .features-inner { + grid-template-columns: 1fr; + } + .features-lottie { + display: none; + } + .why-inner { + grid-template-columns: 1fr; + } + .stats-inner { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/public/css/theme.css b/public/css/theme.css new file mode 100644 index 0000000..9026a91 --- /dev/null +++ b/public/css/theme.css @@ -0,0 +1,66 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +:root { + --card: #262626; + --ring: #f59e0b; + --input: #404040; + --muted: #262626; + --accent: #92400e; + --border: #404040; + --chart-1: #fbbf24; + --chart-2: #d97706; + --chart-3: #92400e; + --chart-4: #b45309; + --chart-5: #92400e; + --popover: #262626; + --primary: #f59e0b; + --sidebar: #0f0f0f; + --secondary: #262626; + --background: #171717; + --foreground: #e5e5e5; + --destructive: #ef4444; + --sidebar-ring: #f59e0b; + --sidebar-accent: #92400e; + --sidebar-border: #404040; + --card-foreground: #e5e5e5; + --sidebar-primary: #f59e0b; + --muted-foreground: #a3a3a3; + --accent-foreground: #fde68a; + --popover-foreground: #e5e5e5; + --primary-foreground: #000000; + --sidebar-foreground: #e5e5e5; + --secondary-foreground: #e5e5e5; + --destructive-foreground: #ffffff; + --sidebar-accent-foreground: #fde68a; + --sidebar-primary-foreground: #ffffff; + --radius: 0.375rem; + --font-sans: Inter, system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; + --color-up: #22c55e; + --color-down: #ef4444; +} + +*, *::before, *::after { + box-sizing: border-box; +} + +body { + background-color: var(--background); + color: var(--foreground); + font-family: var(--font-sans); + margin: 0; + padding: 0; +} + +.animate-in { + filter: blur(4px); + transform: translateY(-8px); + opacity: 0; + transition: filter 0.8s ease, transform 0.8s ease, opacity 0.8s ease; +} + +.animate-in.visible { + filter: blur(0); + transform: translateY(0); + opacity: 1; +} diff --git a/public/css/watchlist.css b/public/css/watchlist.css index 2be941c..6ce0f59 100644 --- a/public/css/watchlist.css +++ b/public/css/watchlist.css @@ -1,556 +1,556 @@ -/* Watchlist Page Styles */ - -.watchlist-container { - max-width: 1400px; - margin: 0 auto; - padding: 30px 20px; -} - -.watchlist-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 30px; - flex-wrap: wrap; - gap: 20px; -} - -.watchlist-header h1 { - color: #ffffff; - font-size: 2rem; - margin: 0; -} - -.watchlist-header p { - color: #8b949e; - margin: 5px 0 0 0; -} - -.btn-add-coin { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 24px; - background: linear-gradient(135deg, #fd7e14 0%, #e55a00 100%); - border: none; - border-radius: 10px; - color: white; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; -} - -.btn-add-coin:hover { - background: linear-gradient(135deg, #e55a00 0%, #cc4a00 100%); - transform: translateY(-2px); - box-shadow: 0 10px 30px rgba(253, 126, 20, 0.3); -} - -/* Watchlist Content */ -.watchlist-content { - background: #1e2a3a; - border-radius: 15px; - border: 1px solid #30363d; - overflow: hidden; -} - -/* Empty State */ -.empty-state { - text-align: center; - padding: 80px 20px; - color: #8b949e; -} - -.empty-state svg { - margin-bottom: 20px; - opacity: 0.5; - stroke: #fd7e14; -} - -.empty-state h3 { - color: #c9d1d9; - margin-bottom: 10px; - font-size: 1.3rem; -} - -.empty-state p { - margin-bottom: 25px; -} - -/* Watchlist Table */ -.watchlist-table-container { - overflow-x: auto; -} - -.watchlist-table { - width: 100%; - border-collapse: collapse; -} - -.watchlist-table th { - text-align: left; - padding: 16px 12px; - color: #8b949e; - font-weight: 500; - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.5px; - background: rgba(13, 27, 42, 0.5); - border-bottom: 1px solid #30363d; - white-space: nowrap; -} - -.watchlist-table td { - padding: 16px 12px; - color: #c9d1d9; - border-bottom: 1px solid #30363d; - white-space: nowrap; -} - -.watchlist-table tbody tr:hover { - background: rgba(253, 126, 20, 0.05); -} - -.watchlist-table tbody tr:last-child td { - border-bottom: none; -} - -/* Column widths */ -.th-rank, .td-rank { - width: 50px; - text-align: center; -} - -.td-rank { - color: #8b949e; - font-weight: 500; -} - -.th-coin { - min-width: 200px; -} - -.th-price, .th-change, .th-mcap, .th-volume { - text-align: right; -} - -.td-price, .td-mcap, .td-volume { - text-align: right; - font-weight: 500; -} - -.td-change { - text-align: right; -} - -.th-actions { - text-align: center; - width: 140px; -} - -.td-actions { - text-align: center; -} - -/* Coin Info */ -.coin-info { - display: flex; - align-items: center; - gap: 12px; -} - -.coin-icon { - width: 36px; - height: 36px; - border-radius: 50%; -} - -.coin-name { - font-weight: 600; - color: #ffffff; -} - -.coin-symbol { - color: #8b949e; - font-size: 0.85rem; - text-transform: uppercase; -} - -/* Change Badge */ -.change-badge { - display: inline-block; - padding: 4px 8px; - border-radius: 6px; - font-size: 0.85rem; - font-weight: 600; -} - -.change-badge.up { - color: #2ecc71; - background: rgba(46, 204, 113, 0.15); -} - -.change-badge.down { - color: #e74c3c; - background: rgba(231, 76, 60, 0.15); -} - -/* Action Buttons */ -.action-buttons { - display: flex; - gap: 8px; - justify-content: center; -} - -.btn-action { - width: 34px; - height: 34px; - border: none; - border-radius: 8px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; -} - -.btn-alert { - background: rgba(241, 196, 15, 0.1); - color: #f1c40f; -} - -.btn-alert:hover { - background: rgba(241, 196, 15, 0.2); -} - -.btn-add-portfolio { - background: rgba(46, 204, 113, 0.1); - color: #2ecc71; -} - -.btn-add-portfolio:hover { - background: rgba(46, 204, 113, 0.2); -} - -.btn-remove { - background: rgba(231, 76, 60, 0.1); - color: #e74c3c; -} - -.btn-remove:hover { - background: rgba(231, 76, 60, 0.2); -} - -/* Legend */ -.watchlist-legend { - display: flex; - gap: 24px; - padding: 16px 20px; - background: rgba(13, 27, 42, 0.5); - border-top: 1px solid #30363d; - flex-wrap: wrap; -} - -.legend-item { - display: flex; - align-items: center; - gap: 8px; - color: #8b949e; - font-size: 0.8rem; -} - -.legend-item svg { - opacity: 0.7; -} - -/* Alert Modal Styles */ -.modal-alert .modal-body { - padding: 25px; -} - -.alert-coin-info { - display: flex; - justify-content: space-between; - align-items: center; - padding: 15px; - background: rgba(13, 27, 42, 0.5); - border-radius: 10px; - margin-bottom: 25px; -} - -#alertCoinName { - color: #ffffff; - font-weight: 600; - font-size: 1.1rem; -} - -.alert-current-price { - color: #8b949e; - font-size: 0.9rem; -} - -.alert-options { - display: flex; - flex-direction: column; - gap: 20px; - margin-bottom: 20px; -} - -.alert-option { - display: flex; - flex-direction: column; - gap: 10px; -} - -.alert-checkbox { - display: flex; - align-items: center; - gap: 10px; - color: #c9d1d9; - cursor: pointer; - font-size: 0.95rem; -} - -.alert-checkbox input[type="checkbox"] { - width: 18px; - height: 18px; - accent-color: #fd7e14; -} - -.alert-input-wrapper { - display: flex; - align-items: center; - gap: 8px; - padding-left: 28px; -} - -.currency-symbol { - color: #8b949e; - font-weight: 500; -} - -.alert-input-wrapper .form-control { - flex: 1; - max-width: 200px; -} - -.alert-note { - color: #6e7681; - font-size: 0.8rem; - margin: 15px 0 0 0; - padding: 10px; - background: rgba(13, 27, 42, 0.5); - border-radius: 8px; - border-left: 3px solid #fd7e14; -} - -/* Quick Add Modal */ -.quick-add-coin-info { - display: flex; - align-items: center; - gap: 15px; - padding: 15px; - background: rgba(13, 27, 42, 0.5); - border-radius: 10px; - margin-bottom: 25px; -} - -.coin-icon-large { - width: 48px; - height: 48px; - border-radius: 50%; -} - -.coin-name-large { - color: #ffffff; - font-weight: 600; - font-size: 1.1rem; -} - -.coin-price-large { - color: #2ecc71; - font-size: 0.95rem; - font-weight: 500; -} - -/* Toast Alert Style */ -.toast.alert { - border-color: #f1c40f; - background: rgba(241, 196, 15, 0.15); - color: #f1c40f; -} - -/* Responsive */ -@media (max-width: 1200px) { - .watchlist-table .th-volume, - .watchlist-table .td-volume { - display: none; - } -} - -@media (max-width: 992px) { - .watchlist-table .th-mcap, - .watchlist-table .td-mcap { - display: none; - } -} - -@media (max-width: 768px) { - .watchlist-container { - padding: 20px 15px; - } - - .watchlist-header { - flex-direction: column; - align-items: flex-start; - } - - .watchlist-header h1 { - font-size: 1.6rem; - } - - .watchlist-table .th-change:last-of-type, - .watchlist-table .td-change:last-of-type { - display: none; - } - - .watchlist-table th, - .watchlist-table td { - padding: 12px 8px; - } - - .coin-icon { - width: 32px; - height: 32px; - } - - .coin-info { - gap: 10px; - } - - .coin-name { - font-size: 0.9rem; - } - - .coin-symbol { - font-size: 0.75rem; - } - - .action-buttons { - flex-direction: column; - gap: 6px; - } - - .btn-action { - width: 30px; - height: 30px; - } - - .btn-action svg { - width: 14px; - height: 14px; - } - - .watchlist-legend { - gap: 16px; - padding: 12px 16px; - } - - .legend-item { - font-size: 0.75rem; - } - - .alert-input-wrapper { - padding-left: 0; - } - - .alert-input-wrapper .form-control { - max-width: none; - } -} - -@media (max-width: 576px) { - .watchlist-container { - padding: 15px 10px; - } - - .watchlist-header h1 { - font-size: 1.4rem; - } - - .btn-add-coin { - padding: 10px 18px; - font-size: 0.9rem; - } - - .watchlist-table th { - font-size: 0.7rem; - padding: 10px 6px; - } - - .watchlist-table td { - padding: 10px 6px; - font-size: 0.85rem; - } - - .th-rank, .td-rank { - width: 30px; - padding: 10px 4px; - } - - .coin-icon { - width: 28px; - height: 28px; - } - - .change-badge { - font-size: 0.75rem; - padding: 3px 6px; - } - - .action-buttons { - flex-direction: row; - gap: 4px; - } - - .btn-action { - width: 28px; - height: 28px; - } - - .watchlist-legend { - display: none; - } - - .quick-add-coin-info { - flex-direction: column; - text-align: center; - } - - .alert-coin-info { - flex-direction: column; - text-align: center; - gap: 8px; - } -} - -@media (max-width: 400px) { - .watchlist-table .th-rank, - .watchlist-table .td-rank { - display: none; - } - - .coin-info { - gap: 8px; - } - - .coin-icon { - width: 24px; - height: 24px; - } - - .coin-name { - font-size: 0.85rem; - } -} +/* Watchlist Page Styles */ + +.watchlist-container { + max-width: 1400px; + margin: 0 auto; + padding: 30px 20px; +} + +.watchlist-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + flex-wrap: wrap; + gap: 20px; +} + +.watchlist-header h1 { + color: var(--foreground); + font-size: 2rem; + margin: 0; +} + +.watchlist-header p { + color: var(--muted-foreground); + margin: 5px 0 0 0; +} + +.btn-add-coin { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + background: linear-gradient(135deg, var(--primary) 0%, #d97706 100%); + border: none; + border-radius: 10px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-add-coin:hover { + background: linear-gradient(135deg, #d97706 0%, #b45309 100%); + transform: translateY(-2px); + box-shadow: 0 10px 30px rgba(245, 158, 11, 0.3); +} + +/* Watchlist Content */ +.watchlist-content { + background: var(--card); + border-radius: 15px; + border: 1px solid var(--border); + overflow: hidden; +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 80px 20px; + color: var(--muted-foreground); +} + +.empty-state svg { + margin-bottom: 20px; + opacity: 0.5; + stroke: var(--primary); +} + +.empty-state h3 { + color: var(--card-foreground); + margin-bottom: 10px; + font-size: 1.3rem; +} + +.empty-state p { + margin-bottom: 25px; +} + +/* Watchlist Table */ +.watchlist-table-container { + overflow-x: auto; +} + +.watchlist-table { + width: 100%; + border-collapse: collapse; +} + +.watchlist-table th { + text-align: left; + padding: 16px 12px; + color: var(--muted-foreground); + font-weight: 500; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.5px; + background: rgba(23, 23, 23, 0.5); + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.watchlist-table td { + padding: 16px 12px; + color: var(--card-foreground); + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.watchlist-table tbody tr:hover { + background: rgba(245, 158, 11, 0.05); +} + +.watchlist-table tbody tr:last-child td { + border-bottom: none; +} + +/* Column widths */ +.th-rank, .td-rank { + width: 50px; + text-align: center; +} + +.td-rank { + color: var(--muted-foreground); + font-weight: 500; +} + +.th-coin { + min-width: 200px; +} + +.th-price, .th-change, .th-mcap, .th-volume { + text-align: right; +} + +.td-price, .td-mcap, .td-volume { + text-align: right; + font-weight: 500; +} + +.td-change { + text-align: right; +} + +.th-actions { + text-align: center; + width: 140px; +} + +.td-actions { + text-align: center; +} + +/* Coin Info */ +.coin-info { + display: flex; + align-items: center; + gap: 12px; +} + +.coin-icon { + width: 36px; + height: 36px; + border-radius: 50%; +} + +.coin-name { + font-weight: 600; + color: var(--foreground); +} + +.coin-symbol { + color: var(--muted-foreground); + font-size: 0.85rem; + text-transform: uppercase; +} + +/* Change Badge */ +.change-badge { + display: inline-block; + padding: 4px 8px; + border-radius: 6px; + font-size: 0.85rem; + font-weight: 600; +} + +.change-badge.up { + color: var(--color-up); + background: rgba(34, 197, 94, 0.15); +} + +.change-badge.down { + color: var(--color-down); + background: rgba(239, 68, 68, 0.15); +} + +/* Action Buttons */ +.action-buttons { + display: flex; + gap: 8px; + justify-content: center; +} + +.btn-action { + width: 34px; + height: 34px; + border: none; + border-radius: 8px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.btn-alert { + background: rgba(251, 191, 36, 0.1); + color: var(--chart-1); +} + +.btn-alert:hover { + background: rgba(251, 191, 36, 0.2); +} + +.btn-add-portfolio { + background: rgba(34, 197, 94, 0.1); + color: var(--color-up); +} + +.btn-add-portfolio:hover { + background: rgba(34, 197, 94, 0.2); +} + +.btn-remove { + background: rgba(239, 68, 68, 0.1); + color: var(--color-down); +} + +.btn-remove:hover { + background: rgba(239, 68, 68, 0.2); +} + +/* Legend */ +.watchlist-legend { + display: flex; + gap: 24px; + padding: 16px 20px; + background: rgba(23, 23, 23, 0.5); + border-top: 1px solid var(--border); + flex-wrap: wrap; +} + +.legend-item { + display: flex; + align-items: center; + gap: 8px; + color: var(--muted-foreground); + font-size: 0.8rem; +} + +.legend-item svg { + opacity: 0.7; +} + +/* Alert Modal Styles */ +.modal-alert .modal-body { + padding: 25px; +} + +.alert-coin-info { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px; + background: rgba(23, 23, 23, 0.5); + border-radius: 10px; + margin-bottom: 25px; +} + +#alertCoinName { + color: var(--foreground); + font-weight: 600; + font-size: 1.1rem; +} + +.alert-current-price { + color: var(--muted-foreground); + font-size: 0.9rem; +} + +.alert-options { + display: flex; + flex-direction: column; + gap: 20px; + margin-bottom: 20px; +} + +.alert-option { + display: flex; + flex-direction: column; + gap: 10px; +} + +.alert-checkbox { + display: flex; + align-items: center; + gap: 10px; + color: var(--card-foreground); + cursor: pointer; + font-size: 0.95rem; +} + +.alert-checkbox input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--primary); +} + +.alert-input-wrapper { + display: flex; + align-items: center; + gap: 8px; + padding-left: 28px; +} + +.currency-symbol { + color: var(--muted-foreground); + font-weight: 500; +} + +.alert-input-wrapper .form-control { + flex: 1; + max-width: 200px; +} + +.alert-note { + color: #6e7681; + font-size: 0.8rem; + margin: 15px 0 0 0; + padding: 10px; + background: rgba(23, 23, 23, 0.5); + border-radius: 8px; + border-left: 3px solid var(--primary); +} + +/* Quick Add Modal */ +.quick-add-coin-info { + display: flex; + align-items: center; + gap: 15px; + padding: 15px; + background: rgba(23, 23, 23, 0.5); + border-radius: 10px; + margin-bottom: 25px; +} + +.coin-icon-large { + width: 48px; + height: 48px; + border-radius: 50%; +} + +.coin-name-large { + color: var(--foreground); + font-weight: 600; + font-size: 1.1rem; +} + +.coin-price-large { + color: var(--color-up); + font-size: 0.95rem; + font-weight: 500; +} + +/* Toast Alert Style */ +.toast.alert { + border-color: var(--chart-1); + background: rgba(251, 191, 36, 0.15); + color: var(--chart-1); +} + +/* Responsive */ +@media (max-width: 1200px) { + .watchlist-table .th-volume, + .watchlist-table .td-volume { + display: none; + } +} + +@media (max-width: 992px) { + .watchlist-table .th-mcap, + .watchlist-table .td-mcap { + display: none; + } +} + +@media (max-width: 768px) { + .watchlist-container { + padding: 20px 15px; + } + + .watchlist-header { + flex-direction: column; + align-items: flex-start; + } + + .watchlist-header h1 { + font-size: 1.6rem; + } + + .watchlist-table .th-change:last-of-type, + .watchlist-table .td-change:last-of-type { + display: none; + } + + .watchlist-table th, + .watchlist-table td { + padding: 12px 8px; + } + + .coin-icon { + width: 32px; + height: 32px; + } + + .coin-info { + gap: 10px; + } + + .coin-name { + font-size: 0.9rem; + } + + .coin-symbol { + font-size: 0.75rem; + } + + .action-buttons { + flex-direction: column; + gap: 6px; + } + + .btn-action { + width: 30px; + height: 30px; + } + + .btn-action svg { + width: 14px; + height: 14px; + } + + .watchlist-legend { + gap: 16px; + padding: 12px 16px; + } + + .legend-item { + font-size: 0.75rem; + } + + .alert-input-wrapper { + padding-left: 0; + } + + .alert-input-wrapper .form-control { + max-width: none; + } +} + +@media (max-width: 576px) { + .watchlist-container { + padding: 15px 10px; + } + + .watchlist-header h1 { + font-size: 1.4rem; + } + + .btn-add-coin { + padding: 10px 18px; + font-size: 0.9rem; + } + + .watchlist-table th { + font-size: 0.7rem; + padding: 10px 6px; + } + + .watchlist-table td { + padding: 10px 6px; + font-size: 0.85rem; + } + + .th-rank, .td-rank { + width: 30px; + padding: 10px 4px; + } + + .coin-icon { + width: 28px; + height: 28px; + } + + .change-badge { + font-size: 0.75rem; + padding: 3px 6px; + } + + .action-buttons { + flex-direction: row; + gap: 4px; + } + + .btn-action { + width: 28px; + height: 28px; + } + + .watchlist-legend { + display: none; + } + + .quick-add-coin-info { + flex-direction: column; + text-align: center; + } + + .alert-coin-info { + flex-direction: column; + text-align: center; + gap: 8px; + } +} + +@media (max-width: 400px) { + .watchlist-table .th-rank, + .watchlist-table .td-rank { + display: none; + } + + .coin-info { + gap: 8px; + } + + .coin-icon { + width: 24px; + height: 24px; + } + + .coin-name { + font-size: 0.85rem; + } +} diff --git a/public/images/exchange.svg b/public/images/exchange.svg index cdb36ab..faca71c 100644 --- a/public/images/exchange.svg +++ b/public/images/exchange.svg @@ -1,44 +1,44 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/js/markets.js b/public/js/markets.js index 5c725ca..b144f50 100644 --- a/public/js/markets.js +++ b/public/js/markets.js @@ -1,68 +1,84 @@ -document.addEventListener("DOMContentLoaded", function() { - const tableBody = document.getElementById("market-data-body"); - const loadingIndicator = document.getElementById("loading-indicator"); - - // Format number as currency - function formatCurrency(num) { - if (num === null || num === undefined) return 'N/A'; - return '$' + num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); - } - - // Format large numbers (market cap, volume) - function formatLargeNumber(num) { - if (num === null || num === undefined) return 'N/A'; - return '$' + num.toLocaleString('en-US'); - } - - async function fetchMarketData() { - const apiUrl = '/api/markets'; - - try { - const response = await fetch(apiUrl); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - // Hide loading indicator - loadingIndicator.style.display = 'none'; - - // Clear existing table rows - tableBody.innerHTML = ''; - - // Populate table with new data - data.forEach(coin => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${coin.market_cap_rank} - -
- ${coin.name} -
-
${coin.name}
-
${coin.symbol.toUpperCase()}
-
-
- - ${formatCurrency(coin.current_price)} - ${formatLargeNumber(coin.market_cap)} - ${formatLargeNumber(coin.total_volume)} - `; - tableBody.appendChild(row); - }); - } catch (error) { - console.error('Error fetching market data:', error); - - // Show error message to user - loadingIndicator.innerHTML = ` -

Failed to load market data. Please try again later.

- - `; - } - } - - // Initial fetch - fetchMarketData(); -}); +document.addEventListener('DOMContentLoaded', function () { + var tableBody = document.getElementById('market-data-body'); + var loadingIndicator = document.getElementById('loading-indicator'); + + function formatCurrency(num) { + if (num === null || num === undefined) return 'N/A'; + return '$' + num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function formatLargeNumber(num) { + if (num === null || num === undefined) return 'N/A'; + if (num >= 1e12) return '$' + (num / 1e12).toFixed(2) + 'T'; + if (num >= 1e9) return '$' + (num / 1e9).toFixed(2) + 'B'; + if (num >= 1e6) return '$' + (num / 1e6).toFixed(2) + 'M'; + return '$' + num.toLocaleString('en-US'); + } + + function renderTrendingCards(coins) { + var container = document.getElementById('trending-cards'); + if (!container) return; + var top3 = coins.slice(0, 3); + container.innerHTML = top3.map(function (coin, i) { + var change = coin.price_change_percentage_24h || 0; + var badgeClass = change >= 0 ? 'badge-up' : 'badge-down'; + var sign = change >= 0 ? '+' : ''; + return ''; + }).join(''); + } + + function buildRow(coin, index) { + var change = coin.price_change_percentage_24h || 0; + var badgeClass = change >= 0 ? 'badge-up' : 'badge-down'; + var sign = change >= 0 ? '+' : ''; + return '' + + '' + (index + 1) + '' + + '
' + + '' + coin.name + '' + + '
' + + '
' + coin.name + '
' + + '
' + coin.symbol.toUpperCase() + '
' + + '
' + + '
' + + '' + formatCurrency(coin.current_price) + '' + + '' + sign + change.toFixed(2) + '%' + + '' + formatLargeNumber(coin.market_cap) + '' + + '' + formatLargeNumber(coin.total_volume) + '' + + ''; + } + + async function fetchMarketData() { + try { + var response = await fetch('/api/markets'); + if (!response.ok) throw new Error('HTTP ' + response.status); + var data = await response.json(); + + loadingIndicator.style.display = 'none'; + + renderTrendingCards(data); + + tableBody.innerHTML = ''; + data.forEach(function (coin, index) { + tableBody.insertAdjacentHTML('beforeend', buildRow(coin, index)); + }); + } catch (error) { + console.error('Error fetching market data:', error); + loadingIndicator.innerHTML = + '

Failed to load market data.

' + + ''; + } + } + + fetchMarketData(); +}); diff --git a/public/js/observer.js b/public/js/observer.js new file mode 100644 index 0000000..9b1ba24 --- /dev/null +++ b/public/js/observer.js @@ -0,0 +1,48 @@ +(function () { + var animObserver = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + animObserver.unobserve(entry.target); + } + }); + }, + { threshold: 0.1 } + ); + + document.querySelectorAll('.animate-in').forEach(function (el) { + animObserver.observe(el); + }); + + var countObserver = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + countUp(entry.target); + countObserver.unobserve(entry.target); + } + }); + }, + { threshold: 0.5 } + ); + + document.querySelectorAll('.count-up').forEach(function (el) { + countObserver.observe(el); + }); + + function countUp(el) { + var target = parseInt(el.dataset.target, 10); + var suffix = el.dataset.suffix || ''; + var duration = 1500; + var start = performance.now(); + function update(now) { + var elapsed = now - start; + var progress = Math.min(elapsed / duration, 1); + var eased = 1 - Math.pow(1 - progress, 3); + el.textContent = Math.floor(eased * target).toLocaleString() + suffix; + if (progress < 1) requestAnimationFrame(update); + } + requestAnimationFrame(update); + } +})(); diff --git a/public/js/portfolio.js b/public/js/portfolio.js index 55b8acc..ead3f39 100644 --- a/public/js/portfolio.js +++ b/public/js/portfolio.js @@ -1,334 +1,334 @@ -// Portfolio JavaScript - -let selectedCoin = null; -let editingHoldingId = null; -let deleteHoldingId = null; -let searchTimeout = null; - -// Initialize -document.addEventListener('DOMContentLoaded', function() { - const coinSearch = document.getElementById('coinSearch'); - if (coinSearch) { - coinSearch.addEventListener('input', handleCoinSearch); - coinSearch.addEventListener('focus', function() { - if (this.value.length > 0) { - document.getElementById('searchResults').classList.add('show'); - } - }); - } - - // Close search results when clicking outside - document.addEventListener('click', function(e) { - const searchWrapper = document.querySelector('.search-wrapper'); - if (searchWrapper && !searchWrapper.contains(e.target)) { - document.getElementById('searchResults').classList.remove('show'); - } - }); - - // Set default date to today - const dateInput = document.getElementById('purchaseDate'); - if (dateInput) { - dateInput.value = new Date().toISOString().split('T')[0]; - } -}); - -// Handle coin search -async function handleCoinSearch(e) { - const query = e.target.value.trim(); - const resultsDiv = document.getElementById('searchResults'); - - if (query.length < 1) { - resultsDiv.classList.remove('show'); - return; - } - - // Debounce - clearTimeout(searchTimeout); - searchTimeout = setTimeout(async () => { - try { - const response = await fetch(`/api/coins/search?q=${encodeURIComponent(query)}`); - const coins = await response.json(); - - if (coins.length === 0) { - resultsDiv.innerHTML = '
No coins found
'; - } else { - resultsDiv.innerHTML = coins.map(coin => ` -
- ${coin.name} -
-
${coin.name}
-
${coin.symbol}
-
-
$${coin.current_price?.toLocaleString() || 'N/A'}
-
- `).join(''); - } - - resultsDiv.classList.add('show'); - } catch (error) { - console.error('Error searching coins:', error); - } - }, 300); -} - -// Select a coin from search results -function selectCoin(coin) { - selectedCoin = coin; - - // Update hidden fields - document.getElementById('coinId').value = coin.id; - document.getElementById('coinImage').value = coin.image; - - // Show selected coin - document.getElementById('selectedCoin').style.display = 'flex'; - document.getElementById('selectedCoinImage').src = coin.image; - document.getElementById('selectedCoinName').textContent = `${coin.name} (${coin.symbol})`; - - // Hide search - document.getElementById('coinSearch').value = ''; - document.getElementById('coinSearch').style.display = 'none'; - document.getElementById('searchResults').classList.remove('show'); - - // Set current price as default - if (coin.current_price) { - document.getElementById('purchasePrice').value = coin.current_price; - } -} - -// Clear coin selection -function clearCoinSelection() { - selectedCoin = null; - document.getElementById('coinId').value = ''; - document.getElementById('coinImage').value = ''; - document.getElementById('selectedCoin').style.display = 'none'; - document.getElementById('coinSearch').style.display = 'block'; - document.getElementById('coinSearch').value = ''; -} - -// Use current price -async function useCurrentPrice() { - if (!selectedCoin) { - showToast('Please select a coin first', 'error'); - return; - } - - try { - const response = await fetch(`/api/coins/price/${selectedCoin.id}`); - const data = await response.json(); - - if (data[selectedCoin.id]?.usd) { - document.getElementById('purchasePrice').value = data[selectedCoin.id].usd; - } - } catch (error) { - console.error('Error fetching price:', error); - showToast('Failed to fetch current price', 'error'); - } -} - -// Open add modal -function openAddModal() { - editingHoldingId = null; - selectedCoin = null; - - // Reset form - document.getElementById('holdingForm').reset(); - document.getElementById('holdingId').value = ''; - document.getElementById('coinId').value = ''; - document.getElementById('coinImage').value = ''; - document.getElementById('selectedCoin').style.display = 'none'; - document.getElementById('coinSearch').style.display = 'block'; - document.getElementById('purchaseDate').value = new Date().toISOString().split('T')[0]; - - // Update modal title and button - document.getElementById('modalTitle').textContent = 'Add New Holding'; - document.getElementById('saveBtn').textContent = 'Add Holding'; - - // Show modal - document.getElementById('holdingModal').style.display = 'flex'; -} - -// Open edit modal -function openEditModal(holding) { - editingHoldingId = holding.id; - selectedCoin = { - id: holding.coinId, - symbol: holding.symbol, - name: holding.name, - image: holding.image - }; - - // Fill form - document.getElementById('holdingId').value = holding.id; - document.getElementById('coinId').value = holding.coinId; - document.getElementById('coinImage').value = holding.image; - document.getElementById('quantity').value = holding.quantity; - document.getElementById('purchasePrice').value = holding.purchasePrice; - document.getElementById('purchaseDate').value = holding.purchaseDate; - document.getElementById('notes').value = holding.notes || ''; - - // Show selected coin - document.getElementById('selectedCoin').style.display = 'flex'; - document.getElementById('selectedCoinImage').src = holding.image; - document.getElementById('selectedCoinName').textContent = `${holding.name} (${holding.symbol})`; - document.getElementById('coinSearch').style.display = 'none'; - - // Update modal title and button - document.getElementById('modalTitle').textContent = 'Edit Holding'; - document.getElementById('saveBtn').textContent = 'Save Changes'; - - // Show modal - document.getElementById('holdingModal').style.display = 'flex'; -} - -// Close modal -function closeModal() { - document.getElementById('holdingModal').style.display = 'none'; -} - -// Save holding (add or edit) -async function saveHolding(event) { - event.preventDefault(); - - const coinId = document.getElementById('coinId').value; - const quantity = document.getElementById('quantity').value; - const purchasePrice = document.getElementById('purchasePrice').value; - - if (!coinId || !selectedCoin) { - showToast('Please select a coin', 'error'); - return; - } - - if (!quantity || parseFloat(quantity) <= 0) { - showToast('Please enter a valid quantity', 'error'); - return; - } - - if (!purchasePrice || parseFloat(purchasePrice) <= 0) { - showToast('Please enter a valid purchase price', 'error'); - return; - } - - const data = { - coinId: selectedCoin.id, - symbol: selectedCoin.symbol, - name: selectedCoin.name, - image: selectedCoin.image, - quantity: parseFloat(quantity), - purchasePrice: parseFloat(purchasePrice), - purchaseDate: document.getElementById('purchaseDate').value, - notes: document.getElementById('notes').value - }; - - try { - let response; - if (editingHoldingId) { - // Edit existing - response = await fetch(`/portfolio/holdings/${editingHoldingId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }); - } else { - // Add new - response = await fetch('/portfolio/holdings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }); - } - - const result = await response.json(); - - if (result.success) { - showToast(editingHoldingId ? 'Holding updated successfully' : 'Holding added successfully', 'success'); - closeModal(); - // Reload page to show updated data - setTimeout(() => location.reload(), 500); - } else { - showToast(result.error || 'Failed to save holding', 'error'); - } - } catch (error) { - console.error('Error saving holding:', error); - showToast('Failed to save holding', 'error'); - } -} - -// Delete holding -function deleteHolding(id) { - deleteHoldingId = id; - document.getElementById('deleteModal').style.display = 'flex'; - - // Set up confirm button - document.getElementById('confirmDeleteBtn').onclick = confirmDelete; -} - -// Close delete modal -function closeDeleteModal() { - document.getElementById('deleteModal').style.display = 'none'; - deleteHoldingId = null; -} - -// Confirm delete -async function confirmDelete() { - if (!deleteHoldingId) return; - - try { - const response = await fetch(`/portfolio/holdings/${deleteHoldingId}`, { - method: 'DELETE' - }); - - const result = await response.json(); - - if (result.success) { - showToast('Holding deleted successfully', 'success'); - closeDeleteModal(); - // Reload page - setTimeout(() => location.reload(), 500); - } else { - showToast(result.error || 'Failed to delete holding', 'error'); - } - } catch (error) { - console.error('Error deleting holding:', error); - showToast('Failed to delete holding', 'error'); - } -} - -// Show toast notification -function showToast(message, type = 'success') { - // Remove existing toast - const existingToast = document.querySelector('.toast'); - if (existingToast) { - existingToast.remove(); - } - - // Create new toast - const toast = document.createElement('div'); - toast.className = `toast ${type}`; - toast.textContent = message; - document.body.appendChild(toast); - - // Show toast - setTimeout(() => toast.classList.add('show'), 10); - - // Hide toast after 3 seconds - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => toast.remove(), 300); - }, 3000); -} - -// Close modals on escape key -document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') { - closeModal(); - closeDeleteModal(); - } -}); - -// Close modals on overlay click -document.addEventListener('click', function(e) { - if (e.target.classList.contains('modal-overlay')) { - closeModal(); - closeDeleteModal(); - } -}); +// Portfolio JavaScript + +let selectedCoin = null; +let editingHoldingId = null; +let deleteHoldingId = null; +let searchTimeout = null; + +// Initialize +document.addEventListener('DOMContentLoaded', function() { + const coinSearch = document.getElementById('coinSearch'); + if (coinSearch) { + coinSearch.addEventListener('input', handleCoinSearch); + coinSearch.addEventListener('focus', function() { + if (this.value.length > 0) { + document.getElementById('searchResults').classList.add('show'); + } + }); + } + + // Close search results when clicking outside + document.addEventListener('click', function(e) { + const searchWrapper = document.querySelector('.search-wrapper'); + if (searchWrapper && !searchWrapper.contains(e.target)) { + document.getElementById('searchResults').classList.remove('show'); + } + }); + + // Set default date to today + const dateInput = document.getElementById('purchaseDate'); + if (dateInput) { + dateInput.value = new Date().toISOString().split('T')[0]; + } +}); + +// Handle coin search +async function handleCoinSearch(e) { + const query = e.target.value.trim(); + const resultsDiv = document.getElementById('searchResults'); + + if (query.length < 1) { + resultsDiv.classList.remove('show'); + return; + } + + // Debounce + clearTimeout(searchTimeout); + searchTimeout = setTimeout(async () => { + try { + const response = await fetch(`/api/coins/search?q=${encodeURIComponent(query)}`); + const coins = await response.json(); + + if (coins.length === 0) { + resultsDiv.innerHTML = '
No coins found
'; + } else { + resultsDiv.innerHTML = coins.map(coin => ` +
+ ${coin.name} +
+
${coin.name}
+
${coin.symbol}
+
+
$${coin.current_price?.toLocaleString() || 'N/A'}
+
+ `).join(''); + } + + resultsDiv.classList.add('show'); + } catch (error) { + console.error('Error searching coins:', error); + } + }, 300); +} + +// Select a coin from search results +function selectCoin(coin) { + selectedCoin = coin; + + // Update hidden fields + document.getElementById('coinId').value = coin.id; + document.getElementById('coinImage').value = coin.image; + + // Show selected coin + document.getElementById('selectedCoin').style.display = 'flex'; + document.getElementById('selectedCoinImage').src = coin.image; + document.getElementById('selectedCoinName').textContent = `${coin.name} (${coin.symbol})`; + + // Hide search + document.getElementById('coinSearch').value = ''; + document.getElementById('coinSearch').style.display = 'none'; + document.getElementById('searchResults').classList.remove('show'); + + // Set current price as default + if (coin.current_price) { + document.getElementById('purchasePrice').value = coin.current_price; + } +} + +// Clear coin selection +function clearCoinSelection() { + selectedCoin = null; + document.getElementById('coinId').value = ''; + document.getElementById('coinImage').value = ''; + document.getElementById('selectedCoin').style.display = 'none'; + document.getElementById('coinSearch').style.display = 'block'; + document.getElementById('coinSearch').value = ''; +} + +// Use current price +async function useCurrentPrice() { + if (!selectedCoin) { + showToast('Please select a coin first', 'error'); + return; + } + + try { + const response = await fetch(`/api/coins/price/${selectedCoin.id}`); + const data = await response.json(); + + if (data[selectedCoin.id]?.usd) { + document.getElementById('purchasePrice').value = data[selectedCoin.id].usd; + } + } catch (error) { + console.error('Error fetching price:', error); + showToast('Failed to fetch current price', 'error'); + } +} + +// Open add modal +function openAddModal() { + editingHoldingId = null; + selectedCoin = null; + + // Reset form + document.getElementById('holdingForm').reset(); + document.getElementById('holdingId').value = ''; + document.getElementById('coinId').value = ''; + document.getElementById('coinImage').value = ''; + document.getElementById('selectedCoin').style.display = 'none'; + document.getElementById('coinSearch').style.display = 'block'; + document.getElementById('purchaseDate').value = new Date().toISOString().split('T')[0]; + + // Update modal title and button + document.getElementById('modalTitle').textContent = 'Add New Holding'; + document.getElementById('saveBtn').textContent = 'Add Holding'; + + // Show modal + document.getElementById('holdingModal').style.display = 'flex'; +} + +// Open edit modal +function openEditModal(holding) { + editingHoldingId = holding.id; + selectedCoin = { + id: holding.coinId, + symbol: holding.symbol, + name: holding.name, + image: holding.image + }; + + // Fill form + document.getElementById('holdingId').value = holding.id; + document.getElementById('coinId').value = holding.coinId; + document.getElementById('coinImage').value = holding.image; + document.getElementById('quantity').value = holding.quantity; + document.getElementById('purchasePrice').value = holding.purchasePrice; + document.getElementById('purchaseDate').value = holding.purchaseDate; + document.getElementById('notes').value = holding.notes || ''; + + // Show selected coin + document.getElementById('selectedCoin').style.display = 'flex'; + document.getElementById('selectedCoinImage').src = holding.image; + document.getElementById('selectedCoinName').textContent = `${holding.name} (${holding.symbol})`; + document.getElementById('coinSearch').style.display = 'none'; + + // Update modal title and button + document.getElementById('modalTitle').textContent = 'Edit Holding'; + document.getElementById('saveBtn').textContent = 'Save Changes'; + + // Show modal + document.getElementById('holdingModal').style.display = 'flex'; +} + +// Close modal +function closeModal() { + document.getElementById('holdingModal').style.display = 'none'; +} + +// Save holding (add or edit) +async function saveHolding(event) { + event.preventDefault(); + + const coinId = document.getElementById('coinId').value; + const quantity = document.getElementById('quantity').value; + const purchasePrice = document.getElementById('purchasePrice').value; + + if (!coinId || !selectedCoin) { + showToast('Please select a coin', 'error'); + return; + } + + if (!quantity || parseFloat(quantity) <= 0) { + showToast('Please enter a valid quantity', 'error'); + return; + } + + if (!purchasePrice || parseFloat(purchasePrice) <= 0) { + showToast('Please enter a valid purchase price', 'error'); + return; + } + + const data = { + coinId: selectedCoin.id, + symbol: selectedCoin.symbol, + name: selectedCoin.name, + image: selectedCoin.image, + quantity: parseFloat(quantity), + purchasePrice: parseFloat(purchasePrice), + purchaseDate: document.getElementById('purchaseDate').value, + notes: document.getElementById('notes').value + }; + + try { + let response; + if (editingHoldingId) { + // Edit existing + response = await fetch(`/portfolio/holdings/${editingHoldingId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + } else { + // Add new + response = await fetch('/portfolio/holdings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + } + + const result = await response.json(); + + if (result.success) { + showToast(editingHoldingId ? 'Holding updated successfully' : 'Holding added successfully', 'success'); + closeModal(); + // Reload page to show updated data + setTimeout(() => location.reload(), 500); + } else { + showToast(result.error || 'Failed to save holding', 'error'); + } + } catch (error) { + console.error('Error saving holding:', error); + showToast('Failed to save holding', 'error'); + } +} + +// Delete holding +function deleteHolding(id) { + deleteHoldingId = id; + document.getElementById('deleteModal').style.display = 'flex'; + + // Set up confirm button + document.getElementById('confirmDeleteBtn').onclick = confirmDelete; +} + +// Close delete modal +function closeDeleteModal() { + document.getElementById('deleteModal').style.display = 'none'; + deleteHoldingId = null; +} + +// Confirm delete +async function confirmDelete() { + if (!deleteHoldingId) return; + + try { + const response = await fetch(`/portfolio/holdings/${deleteHoldingId}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + showToast('Holding deleted successfully', 'success'); + closeDeleteModal(); + // Reload page + setTimeout(() => location.reload(), 500); + } else { + showToast(result.error || 'Failed to delete holding', 'error'); + } + } catch (error) { + console.error('Error deleting holding:', error); + showToast('Failed to delete holding', 'error'); + } +} + +// Show toast notification +function showToast(message, type = 'success') { + // Remove existing toast + const existingToast = document.querySelector('.toast'); + if (existingToast) { + existingToast.remove(); + } + + // Create new toast + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.textContent = message; + document.body.appendChild(toast); + + // Show toast + setTimeout(() => toast.classList.add('show'), 10); + + // Hide toast after 3 seconds + setTimeout(() => { + toast.classList.remove('show'); + setTimeout(() => toast.remove(), 300); + }, 3000); +} + +// Close modals on escape key +document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeModal(); + closeDeleteModal(); + } +}); + +// Close modals on overlay click +document.addEventListener('click', function(e) { + if (e.target.classList.contains('modal-overlay')) { + closeModal(); + closeDeleteModal(); + } +}); diff --git a/public/js/watchlist.js b/public/js/watchlist.js index 0071b07..2d0ea90 100644 --- a/public/js/watchlist.js +++ b/public/js/watchlist.js @@ -1,357 +1,357 @@ -// Watchlist JavaScript - -let searchTimeout = null; -let removeCoinId = null; - -// Initialize -document.addEventListener('DOMContentLoaded', function() { - const searchInput = document.getElementById('watchlistCoinSearch'); - if (searchInput) { - searchInput.addEventListener('input', handleWatchlistSearch); - searchInput.addEventListener('focus', function() { - if (this.value.length > 0) { - document.getElementById('watchlistSearchResults').classList.add('show'); - } - }); - } - - // Close search results when clicking outside - document.addEventListener('click', function(e) { - const searchWrapper = document.querySelector('#addWatchlistModal .search-wrapper'); - if (searchWrapper && !searchWrapper.contains(e.target)) { - document.getElementById('watchlistSearchResults').classList.remove('show'); - } - }); - - // Check for price alerts - checkPriceAlerts(); -}); - -// Handle coin search for watchlist -async function handleWatchlistSearch(e) { - const query = e.target.value.trim(); - const resultsDiv = document.getElementById('watchlistSearchResults'); - - if (query.length < 1) { - resultsDiv.classList.remove('show'); - return; - } - - clearTimeout(searchTimeout); - searchTimeout = setTimeout(async () => { - try { - const response = await fetch(`/api/coins/search?q=${encodeURIComponent(query)}`); - const coins = await response.json(); - - if (coins.length === 0) { - resultsDiv.innerHTML = '
No coins found
'; - } else { - resultsDiv.innerHTML = coins.map(coin => ` -
- ${coin.name} -
-
${coin.name}
-
${coin.symbol}
-
-
$${coin.current_price?.toLocaleString() || 'N/A'}
-
- `).join(''); - } - - resultsDiv.classList.add('show'); - } catch (error) { - console.error('Error searching coins:', error); - } - }, 300); -} - -// Add coin to watchlist -async function addToWatchlist(coinId, name, symbol, image) { - try { - const response = await fetch('/portfolio/watchlist', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ coinId, name, symbol, image }) - }); - - const result = await response.json(); - - if (result.success) { - showToast('Added to watchlist!', 'success'); - closeAddWatchlistModal(); - setTimeout(() => location.reload(), 500); - } else { - showToast(result.error || 'Failed to add to watchlist', 'error'); - } - } catch (error) { - console.error('Error adding to watchlist:', error); - showToast('Failed to add to watchlist', 'error'); - } -} - -// Remove from watchlist -function removeFromWatchlist(coinId) { - removeCoinId = coinId; - document.getElementById('removeModal').style.display = 'flex'; - document.getElementById('confirmRemoveBtn').onclick = confirmRemove; -} - -async function confirmRemove() { - if (!removeCoinId) return; - - try { - const response = await fetch(`/portfolio/watchlist/${removeCoinId}`, { - method: 'DELETE' - }); - - const result = await response.json(); - - if (result.success) { - showToast('Removed from watchlist', 'success'); - closeRemoveModal(); - setTimeout(() => location.reload(), 500); - } else { - showToast(result.error || 'Failed to remove from watchlist', 'error'); - } - } catch (error) { - console.error('Error removing from watchlist:', error); - showToast('Failed to remove from watchlist', 'error'); - } -} - -// Open Add Watchlist Modal -function openAddWatchlistModal() { - document.getElementById('watchlistCoinSearch').value = ''; - document.getElementById('watchlistSearchResults').classList.remove('show'); - document.getElementById('addWatchlistModal').style.display = 'flex'; - document.getElementById('watchlistCoinSearch').focus(); -} - -// Close Add Watchlist Modal -function closeAddWatchlistModal() { - document.getElementById('addWatchlistModal').style.display = 'none'; -} - -// Close Remove Modal -function closeRemoveModal() { - document.getElementById('removeModal').style.display = 'none'; - removeCoinId = null; -} - -// ==================== PRICE ALERTS ==================== - -// Open Alert Modal -function openAlertModal(coinId, name, symbol, currentPrice) { - document.getElementById('alertCoinId').value = coinId; - document.getElementById('alertCoinName').textContent = `${name} (${symbol})`; - document.getElementById('alertCurrentPrice').textContent = `Current: $${currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: currentPrice < 1 ? 6 : 2})}`; - - // Load existing alerts from localStorage - const alerts = JSON.parse(localStorage.getItem('priceAlerts') || '{}'); - const coinAlert = alerts[coinId]; - - if (coinAlert) { - document.getElementById('alertAboveEnabled').checked = coinAlert.above !== null; - document.getElementById('alertAbovePrice').value = coinAlert.above || ''; - document.getElementById('alertBelowEnabled').checked = coinAlert.below !== null; - document.getElementById('alertBelowPrice').value = coinAlert.below || ''; - } else { - document.getElementById('alertAboveEnabled').checked = false; - document.getElementById('alertAbovePrice').value = ''; - document.getElementById('alertBelowEnabled').checked = false; - document.getElementById('alertBelowPrice').value = ''; - } - - document.getElementById('alertModal').style.display = 'flex'; -} - -// Close Alert Modal -function closeAlertModal() { - document.getElementById('alertModal').style.display = 'none'; -} - -// Save Alert -function saveAlert() { - const coinId = document.getElementById('alertCoinId').value; - const aboveEnabled = document.getElementById('alertAboveEnabled').checked; - const abovePrice = parseFloat(document.getElementById('alertAbovePrice').value); - const belowEnabled = document.getElementById('alertBelowEnabled').checked; - const belowPrice = parseFloat(document.getElementById('alertBelowPrice').value); - - const alerts = JSON.parse(localStorage.getItem('priceAlerts') || '{}'); - - if (!aboveEnabled && !belowEnabled) { - // Remove alert if both are disabled - delete alerts[coinId]; - } else { - alerts[coinId] = { - above: aboveEnabled && abovePrice ? abovePrice : null, - below: belowEnabled && belowPrice ? belowPrice : null - }; - } - - localStorage.setItem('priceAlerts', JSON.stringify(alerts)); - showToast('Price alert saved!', 'success'); - closeAlertModal(); -} - -// Check Price Alerts -function checkPriceAlerts() { - const alerts = JSON.parse(localStorage.getItem('priceAlerts') || '{}'); - const rows = document.querySelectorAll('.watchlist-table tbody tr'); - - rows.forEach(row => { - const coinId = row.dataset.coinId; - const priceText = row.querySelector('.td-price')?.textContent; - const currentPrice = parseFloat(priceText?.replace(/[$,]/g, '')); - - if (alerts[coinId] && currentPrice) { - const alert = alerts[coinId]; - - if (alert.above && currentPrice >= alert.above) { - showAlertNotification(coinId, 'above', alert.above, currentPrice); - } - - if (alert.below && currentPrice <= alert.below) { - showAlertNotification(coinId, 'below', alert.below, currentPrice); - } - } - }); -} - -// Show Alert Notification -function showAlertNotification(coinId, type, targetPrice, currentPrice) { - const row = document.querySelector(`tr[data-coin-id="${coinId}"]`); - const coinName = row?.querySelector('.coin-name')?.textContent || coinId; - - const message = type === 'above' - ? `${coinName} is now above $${targetPrice.toLocaleString()} (Current: $${currentPrice.toLocaleString()})` - : `${coinName} is now below $${targetPrice.toLocaleString()} (Current: $${currentPrice.toLocaleString()})`; - - // Show toast notification - showToast(message, 'alert'); - - // Mark as triggered (optional: remove or keep the alert) - // For now, we'll keep it active -} - -// ==================== QUICK ADD TO PORTFOLIO ==================== - -// Open Quick Add Modal -function quickAddToPortfolio(coinId, name, symbol, image, currentPrice) { - document.getElementById('quickAddCoinId').value = coinId; - document.getElementById('quickAddCoinSymbol').value = symbol; - document.getElementById('quickAddCoinImageUrl').value = image; - document.getElementById('quickAddCoinCurrentPrice').value = currentPrice; - - document.getElementById('quickAddCoinImage').src = image; - document.getElementById('quickAddCoinName').textContent = `${name} (${symbol})`; - document.getElementById('quickAddCoinPrice').textContent = `$${currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: currentPrice < 1 ? 6 : 2})}`; - - document.getElementById('quickAddQuantity').value = ''; - document.getElementById('quickAddPurchasePrice').value = currentPrice; - - document.getElementById('quickAddModal').style.display = 'flex'; -} - -// Close Quick Add Modal -function closeQuickAddModal() { - document.getElementById('quickAddModal').style.display = 'none'; -} - -// Use current price for quick add -function useQuickAddCurrentPrice() { - const currentPrice = document.getElementById('quickAddCoinCurrentPrice').value; - document.getElementById('quickAddPurchasePrice').value = currentPrice; -} - -// Submit Quick Add -async function submitQuickAdd() { - const coinId = document.getElementById('quickAddCoinId').value; - const symbol = document.getElementById('quickAddCoinSymbol').value; - const image = document.getElementById('quickAddCoinImageUrl').value; - const name = document.getElementById('quickAddCoinName').textContent.split(' (')[0]; - const quantity = parseFloat(document.getElementById('quickAddQuantity').value); - const purchasePrice = parseFloat(document.getElementById('quickAddPurchasePrice').value); - - if (!quantity || quantity <= 0) { - showToast('Please enter a valid quantity', 'error'); - return; - } - - if (!purchasePrice || purchasePrice <= 0) { - showToast('Please enter a valid purchase price', 'error'); - return; - } - - try { - const response = await fetch('/portfolio/holdings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - coinId, - symbol, - name, - image, - quantity, - purchasePrice, - purchaseDate: new Date().toISOString().split('T')[0], - notes: 'Added from watchlist' - }) - }); - - const result = await response.json(); - - if (result.success) { - showToast('Added to portfolio!', 'success'); - closeQuickAddModal(); - } else { - showToast(result.error || 'Failed to add to portfolio', 'error'); - } - } catch (error) { - console.error('Error adding to portfolio:', error); - showToast('Failed to add to portfolio', 'error'); - } -} - -// ==================== UTILITIES ==================== - -// Show toast notification -function showToast(message, type = 'success') { - const existingToast = document.querySelector('.toast'); - if (existingToast) { - existingToast.remove(); - } - - const toast = document.createElement('div'); - toast.className = `toast ${type}`; - toast.textContent = message; - document.body.appendChild(toast); - - setTimeout(() => toast.classList.add('show'), 10); - - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => toast.remove(), 300); - }, 4000); -} - -// Close modals on escape key -document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') { - closeAddWatchlistModal(); - closeAlertModal(); - closeQuickAddModal(); - closeRemoveModal(); - } -}); - -// Close modals on overlay click -document.addEventListener('click', function(e) { - if (e.target.classList.contains('modal-overlay')) { - closeAddWatchlistModal(); - closeAlertModal(); - closeQuickAddModal(); - closeRemoveModal(); - } -}); +// Watchlist JavaScript + +let searchTimeout = null; +let removeCoinId = null; + +// Initialize +document.addEventListener('DOMContentLoaded', function() { + const searchInput = document.getElementById('watchlistCoinSearch'); + if (searchInput) { + searchInput.addEventListener('input', handleWatchlistSearch); + searchInput.addEventListener('focus', function() { + if (this.value.length > 0) { + document.getElementById('watchlistSearchResults').classList.add('show'); + } + }); + } + + // Close search results when clicking outside + document.addEventListener('click', function(e) { + const searchWrapper = document.querySelector('#addWatchlistModal .search-wrapper'); + if (searchWrapper && !searchWrapper.contains(e.target)) { + document.getElementById('watchlistSearchResults').classList.remove('show'); + } + }); + + // Check for price alerts + checkPriceAlerts(); +}); + +// Handle coin search for watchlist +async function handleWatchlistSearch(e) { + const query = e.target.value.trim(); + const resultsDiv = document.getElementById('watchlistSearchResults'); + + if (query.length < 1) { + resultsDiv.classList.remove('show'); + return; + } + + clearTimeout(searchTimeout); + searchTimeout = setTimeout(async () => { + try { + const response = await fetch(`/api/coins/search?q=${encodeURIComponent(query)}`); + const coins = await response.json(); + + if (coins.length === 0) { + resultsDiv.innerHTML = '
No coins found
'; + } else { + resultsDiv.innerHTML = coins.map(coin => ` +
+ ${coin.name} +
+
${coin.name}
+
${coin.symbol}
+
+
$${coin.current_price?.toLocaleString() || 'N/A'}
+
+ `).join(''); + } + + resultsDiv.classList.add('show'); + } catch (error) { + console.error('Error searching coins:', error); + } + }, 300); +} + +// Add coin to watchlist +async function addToWatchlist(coinId, name, symbol, image) { + try { + const response = await fetch('/portfolio/watchlist', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ coinId, name, symbol, image }) + }); + + const result = await response.json(); + + if (result.success) { + showToast('Added to watchlist!', 'success'); + closeAddWatchlistModal(); + setTimeout(() => location.reload(), 500); + } else { + showToast(result.error || 'Failed to add to watchlist', 'error'); + } + } catch (error) { + console.error('Error adding to watchlist:', error); + showToast('Failed to add to watchlist', 'error'); + } +} + +// Remove from watchlist +function removeFromWatchlist(coinId) { + removeCoinId = coinId; + document.getElementById('removeModal').style.display = 'flex'; + document.getElementById('confirmRemoveBtn').onclick = confirmRemove; +} + +async function confirmRemove() { + if (!removeCoinId) return; + + try { + const response = await fetch(`/portfolio/watchlist/${removeCoinId}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + showToast('Removed from watchlist', 'success'); + closeRemoveModal(); + setTimeout(() => location.reload(), 500); + } else { + showToast(result.error || 'Failed to remove from watchlist', 'error'); + } + } catch (error) { + console.error('Error removing from watchlist:', error); + showToast('Failed to remove from watchlist', 'error'); + } +} + +// Open Add Watchlist Modal +function openAddWatchlistModal() { + document.getElementById('watchlistCoinSearch').value = ''; + document.getElementById('watchlistSearchResults').classList.remove('show'); + document.getElementById('addWatchlistModal').style.display = 'flex'; + document.getElementById('watchlistCoinSearch').focus(); +} + +// Close Add Watchlist Modal +function closeAddWatchlistModal() { + document.getElementById('addWatchlistModal').style.display = 'none'; +} + +// Close Remove Modal +function closeRemoveModal() { + document.getElementById('removeModal').style.display = 'none'; + removeCoinId = null; +} + +// ==================== PRICE ALERTS ==================== + +// Open Alert Modal +function openAlertModal(coinId, name, symbol, currentPrice) { + document.getElementById('alertCoinId').value = coinId; + document.getElementById('alertCoinName').textContent = `${name} (${symbol})`; + document.getElementById('alertCurrentPrice').textContent = `Current: $${currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: currentPrice < 1 ? 6 : 2})}`; + + // Load existing alerts from localStorage + const alerts = JSON.parse(localStorage.getItem('priceAlerts') || '{}'); + const coinAlert = alerts[coinId]; + + if (coinAlert) { + document.getElementById('alertAboveEnabled').checked = coinAlert.above !== null; + document.getElementById('alertAbovePrice').value = coinAlert.above || ''; + document.getElementById('alertBelowEnabled').checked = coinAlert.below !== null; + document.getElementById('alertBelowPrice').value = coinAlert.below || ''; + } else { + document.getElementById('alertAboveEnabled').checked = false; + document.getElementById('alertAbovePrice').value = ''; + document.getElementById('alertBelowEnabled').checked = false; + document.getElementById('alertBelowPrice').value = ''; + } + + document.getElementById('alertModal').style.display = 'flex'; +} + +// Close Alert Modal +function closeAlertModal() { + document.getElementById('alertModal').style.display = 'none'; +} + +// Save Alert +function saveAlert() { + const coinId = document.getElementById('alertCoinId').value; + const aboveEnabled = document.getElementById('alertAboveEnabled').checked; + const abovePrice = parseFloat(document.getElementById('alertAbovePrice').value); + const belowEnabled = document.getElementById('alertBelowEnabled').checked; + const belowPrice = parseFloat(document.getElementById('alertBelowPrice').value); + + const alerts = JSON.parse(localStorage.getItem('priceAlerts') || '{}'); + + if (!aboveEnabled && !belowEnabled) { + // Remove alert if both are disabled + delete alerts[coinId]; + } else { + alerts[coinId] = { + above: aboveEnabled && abovePrice ? abovePrice : null, + below: belowEnabled && belowPrice ? belowPrice : null + }; + } + + localStorage.setItem('priceAlerts', JSON.stringify(alerts)); + showToast('Price alert saved!', 'success'); + closeAlertModal(); +} + +// Check Price Alerts +function checkPriceAlerts() { + const alerts = JSON.parse(localStorage.getItem('priceAlerts') || '{}'); + const rows = document.querySelectorAll('.watchlist-table tbody tr'); + + rows.forEach(row => { + const coinId = row.dataset.coinId; + const priceText = row.querySelector('.td-price')?.textContent; + const currentPrice = parseFloat(priceText?.replace(/[$,]/g, '')); + + if (alerts[coinId] && currentPrice) { + const alert = alerts[coinId]; + + if (alert.above && currentPrice >= alert.above) { + showAlertNotification(coinId, 'above', alert.above, currentPrice); + } + + if (alert.below && currentPrice <= alert.below) { + showAlertNotification(coinId, 'below', alert.below, currentPrice); + } + } + }); +} + +// Show Alert Notification +function showAlertNotification(coinId, type, targetPrice, currentPrice) { + const row = document.querySelector(`tr[data-coin-id="${coinId}"]`); + const coinName = row?.querySelector('.coin-name')?.textContent || coinId; + + const message = type === 'above' + ? `${coinName} is now above $${targetPrice.toLocaleString()} (Current: $${currentPrice.toLocaleString()})` + : `${coinName} is now below $${targetPrice.toLocaleString()} (Current: $${currentPrice.toLocaleString()})`; + + // Show toast notification + showToast(message, 'alert'); + + // Mark as triggered (optional: remove or keep the alert) + // For now, we'll keep it active +} + +// ==================== QUICK ADD TO PORTFOLIO ==================== + +// Open Quick Add Modal +function quickAddToPortfolio(coinId, name, symbol, image, currentPrice) { + document.getElementById('quickAddCoinId').value = coinId; + document.getElementById('quickAddCoinSymbol').value = symbol; + document.getElementById('quickAddCoinImageUrl').value = image; + document.getElementById('quickAddCoinCurrentPrice').value = currentPrice; + + document.getElementById('quickAddCoinImage').src = image; + document.getElementById('quickAddCoinName').textContent = `${name} (${symbol})`; + document.getElementById('quickAddCoinPrice').textContent = `$${currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: currentPrice < 1 ? 6 : 2})}`; + + document.getElementById('quickAddQuantity').value = ''; + document.getElementById('quickAddPurchasePrice').value = currentPrice; + + document.getElementById('quickAddModal').style.display = 'flex'; +} + +// Close Quick Add Modal +function closeQuickAddModal() { + document.getElementById('quickAddModal').style.display = 'none'; +} + +// Use current price for quick add +function useQuickAddCurrentPrice() { + const currentPrice = document.getElementById('quickAddCoinCurrentPrice').value; + document.getElementById('quickAddPurchasePrice').value = currentPrice; +} + +// Submit Quick Add +async function submitQuickAdd() { + const coinId = document.getElementById('quickAddCoinId').value; + const symbol = document.getElementById('quickAddCoinSymbol').value; + const image = document.getElementById('quickAddCoinImageUrl').value; + const name = document.getElementById('quickAddCoinName').textContent.split(' (')[0]; + const quantity = parseFloat(document.getElementById('quickAddQuantity').value); + const purchasePrice = parseFloat(document.getElementById('quickAddPurchasePrice').value); + + if (!quantity || quantity <= 0) { + showToast('Please enter a valid quantity', 'error'); + return; + } + + if (!purchasePrice || purchasePrice <= 0) { + showToast('Please enter a valid purchase price', 'error'); + return; + } + + try { + const response = await fetch('/portfolio/holdings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + coinId, + symbol, + name, + image, + quantity, + purchasePrice, + purchaseDate: new Date().toISOString().split('T')[0], + notes: 'Added from watchlist' + }) + }); + + const result = await response.json(); + + if (result.success) { + showToast('Added to portfolio!', 'success'); + closeQuickAddModal(); + } else { + showToast(result.error || 'Failed to add to portfolio', 'error'); + } + } catch (error) { + console.error('Error adding to portfolio:', error); + showToast('Failed to add to portfolio', 'error'); + } +} + +// ==================== UTILITIES ==================== + +// Show toast notification +function showToast(message, type = 'success') { + const existingToast = document.querySelector('.toast'); + if (existingToast) { + existingToast.remove(); + } + + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.textContent = message; + document.body.appendChild(toast); + + setTimeout(() => toast.classList.add('show'), 10); + + setTimeout(() => { + toast.classList.remove('show'); + setTimeout(() => toast.remove(), 300); + }, 4000); +} + +// Close modals on escape key +document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeAddWatchlistModal(); + closeAlertModal(); + closeQuickAddModal(); + closeRemoveModal(); + } +}); + +// Close modals on overlay click +document.addEventListener('click', function(e) { + if (e.target.classList.contains('modal-overlay')) { + closeAddWatchlistModal(); + closeAlertModal(); + closeQuickAddModal(); + closeRemoveModal(); + } +}); diff --git a/views/auth/login.ejs b/views/auth/login.ejs index a1b7cca..51af89c 100644 --- a/views/auth/login.ejs +++ b/views/auth/login.ejs @@ -1,87 +1,89 @@ - - - - - - Login - CryptoPulse - - - - - - <%- include('../partials/header') %> - -
-
-
-

Welcome Back

-

Sign in to access your portfolio

-
- - <% if (error) { %> - - <% } %> - - <% if (success) { %> - - <% } %> - -
-
- -
- - - - - - - -
-
- -
- -
- - - - - - - -
-
- -
- -
- - -
- - - -
-

Demo Account:

-

Email: demo@cryptopulse.com

-

Password: demo123

-
-
-
- - <%- include('../partials/footer') %> - - - + + + + + + Login - CryptoPulse + + + + + + + <%- include('../partials/header') %> + +
+
+
+

Welcome Back

+

Sign in to access your portfolio

+
+ + <% if (error) { %> + + <% } %> + + <% if (success) { %> + + <% } %> + +
+
+ +
+ + + + + + + +
+
+ +
+ +
+ + + + + + + +
+
+ +
+ +
+ + +
+ + + +
+

Demo Account:

+

Email: demo@cryptopulse.com

+

Password: demo123

+
+
+
+ + <%- include('../partials/footer') %> + + + + diff --git a/views/auth/register.ejs b/views/auth/register.ejs index b37ea49..2eb1556 100644 --- a/views/auth/register.ejs +++ b/views/auth/register.ejs @@ -1,94 +1,96 @@ - - - - - - Register - CryptoPulse - - - - - - <%- include('../partials/header') %> - -
-
-
-

Create Account

-

Join CryptoPulse to track your portfolio

-
- - <% if (error) { %> - - <% } %> - -
-
- -
- - - - - - - -
-
- -
- -
- - - - - - - -
-
- -
- -
- - - - - - - -
-
- -
- -
- - - - - - - -
-
- - -
- - -
-
- - <%- include('../partials/footer') %> - - - + + + + + + Register - CryptoPulse + + + + + + + <%- include('../partials/header') %> + +
+
+
+

Create Account

+

Join CryptoPulse to track your portfolio

+
+ + <% if (error) { %> + + <% } %> + +
+
+ +
+ + + + + + + +
+
+ +
+ +
+ + + + + + + +
+
+ +
+ +
+ + + + + + + +
+
+ +
+ +
+ + + + + + + +
+
+ + +
+ + +
+
+ + <%- include('../partials/footer') %> + + + + diff --git a/views/graphs.ejs b/views/graphs.ejs index 34e5d21..bcdc7f3 100644 --- a/views/graphs.ejs +++ b/views/graphs.ejs @@ -1,576 +1,579 @@ - - - - - - Crypto Charts - CryptoPulse - - - - - - <%- include('partials/header') %> - -
-
-

Live Crypto Charts

-

Real-time price charts with historical data

-
- -
- -
-
-
- -
-

Bitcoin

- BTC/USD -
-
-
-
--
-
--
-
-
- -
-
- 24h High - -- -
-
- 24h Low - -- -
-
- 24h Volume - -- -
-
- Market Cap - -- -
-
- -
- - - - - -
- -
-
-
- Loading chart data... -
- -
- - -
- - -
-
-
- -
-

Ethereum

- ETH/USD -
-
-
-
--
-
--
-
-
- -
-
- 24h High - -- -
-
- 24h Low - -- -
-
- 24h Volume - -- -
-
- Market Cap - -- -
-
- -
- - - - - -
- -
-
-
- Loading chart data... -
- -
- - -
- - -
-
-
- -
-

BNB

- BNB/USD -
-
-
-
--
-
--
-
-
- -
-
- 24h High - -- -
-
- 24h Low - -- -
-
- 24h Volume - -- -
-
- Market Cap - -- -
-
- -
- - - - - -
- -
-
-
- Loading chart data... -
- -
- - -
- - -
-
-
- -
-

XRP

- XRP/USD -
-
-
-
--
-
--
-
-
- -
-
- 24h High - -- -
-
- 24h Low - -- -
-
- 24h Volume - -- -
-
- Market Cap - -- -
-
- -
- - - - - -
- -
-
-
- Loading chart data... -
- -
- - -
-
-
- - <%- include('partials/footer') %> - - - - - - - - + + + + + + Crypto Charts - CryptoPulse + + + + + + + <%- include('partials/header') %> + +
+
+

Live Crypto Charts

+

Real-time price charts with historical data

+
+ +
+ +
+
+
+ +
+

Bitcoin

+ BTC/USD +
+
+
+
--
+
--
+
+
+ +
+
+ 24h High + -- +
+
+ 24h Low + -- +
+
+ 24h Volume + -- +
+
+ Market Cap + -- +
+
+ +
+ + + + + +
+ +
+
+
+ Loading chart data... +
+ +
+ + +
+ + +
+
+
+ +
+

Ethereum

+ ETH/USD +
+
+
+
--
+
--
+
+
+ +
+
+ 24h High + -- +
+
+ 24h Low + -- +
+
+ 24h Volume + -- +
+
+ Market Cap + -- +
+
+ +
+ + + + + +
+ +
+
+
+ Loading chart data... +
+ +
+ + +
+ + +
+
+
+ +
+

BNB

+ BNB/USD +
+
+
+
--
+
--
+
+
+ +
+
+ 24h High + -- +
+
+ 24h Low + -- +
+
+ 24h Volume + -- +
+
+ Market Cap + -- +
+
+ +
+ + + + + +
+ +
+
+
+ Loading chart data... +
+ +
+ + +
+ + +
+
+
+ +
+

XRP

+ XRP/USD +
+
+
+
--
+
--
+
+
+ +
+
+ 24h High + -- +
+
+ 24h Low + -- +
+
+ 24h Volume + -- +
+
+ Market Cap + -- +
+
+ +
+ + + + + +
+ +
+
+
+ Loading chart data... +
+ +
+ + +
+
+
+ + <%- include('partials/footer') %> + + + + + + + + + + diff --git a/views/index.ejs b/views/index.ejs index 44528eb..750dfa3 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -3,210 +3,200 @@ - CryptoPulse - - + CryptoPulse — Real-time Crypto Data + - - - - - - + - - <%- include('partials/header') %> - - -
-
-

The Most Reliable & Comprehensive - Cryptocurrency Data API -

-

- Get real-time data on over 20,000 cryptocurrencies. - For Traders, Developers & Enterprises — All in - Real-Time. -

- -
-
- Bitcoin - Ethereum -
-

Fetch crypto prices and market data from 200+ networks, from major blockchains to new launches

-
- - - -
-
-
    -
  • Data for over 17,000+ cryptocurrencies such as Bitcoin, Ethereum tracked across over 1,000+ crypto exchanges like Binance, Crypto.com, and Kraken
  • -
  • Access to real-time crypto price, market data, NFT floor prices, trading volume, metadata, historical data, and more
  • -
  • Get real-time data on over 20,000 cryptocurrencies
  • -
  • Total global crypto market capitalization and 24H trading volume.
  • -
  • BTC & ETH dominance percentages.
  • -
  • Fear and Greed Index integration for market sentiment tracking.
  • -
-
- -
- - -
- - - -
-
    -
  • Candlestick (OHLCV) data — precise open, high, low, close & volume for chart analysis; supports minute/hour/daily granularity where available
  • -
  • Multi-chain & CEX token support — covers thousands of tokens and hundreds of exchanges (e.g. active_exchanges: ~400, active_cryptocurrencies: 3,000+) for global market parity.
  • -
  • NFT collection data — live + historical floor‑prices, trading volume, unique holders & metadata aggregated across 20–30 marketplaces (OpenSea, Blur, Magic Eden) via CoinGecko’s NFT API.
  • - -
-
- - -
- - - -
-
-

Powerful Crypto Data API

-

Access real-time crypto price, market data, NFT floor prices, trading volume, metadata, historical data, and more with our crypto data API.

-
-

10 Billion+

-

API calls per month

+<%- include('partials/header') %> + + +
+
+
+

+ CryptoPulse +

+

+ Real-time data on 20,000+ cryptocurrencies.
+ For Traders, Developers & Enterprises. +

+
- -

70+

-

Endpoints

+
+ +
- -

10+ Years

-

Historical data

+
+
+ + +
+
+
+ 0 + API Calls / Month
- -

200+

-

Networks

+
+ 0 + Endpoints
- -

2000+

-

NFTs Collection

+
+ 0 + Historical Data
- -

30+

-

Marketplaces

+
+ 0 + Networks
- +
+ 0 + NFT Collections
- -
-
-

Supported Networks

-

Extensive Coverage Across Multiple Chains

-

Fetch crypto prices and market data from 200+ networks, from major blockchains to new launches.

+
+ 0 + Marketplaces
-
-
- - -
- -
-
    -
  • Includes top chains like Bitcoin, Ethereum, BNB, Solana, and more.
  • -
  • Stay up-to-date with support for the latest blockchain launches.
  • -
  • Get live prices and market stats from multiple networks in one place.
  • -
  • Pulled from trusted sources like exchanges and on-chain data.
  • -
  • Aggregated from verified exchanges and oracles to ensure quality.
  • -
  • Built for enterprise-grade performance with scalability in mind.
  • -
  • Live market data across all networks — fast, accurate, and reliable.
  • -
-
-
+
+ + +
+
+
+ + +
+
+

Everything You Need

+
    +
  • Multi-chain coverage — Bitcoin, Ethereum, BNB, Solana and 200+ more
  • +
  • Real-time prices — live data updated every 60 seconds
  • +
  • Historical data — 10+ years of OHLCV records
  • +
  • NFT floor prices — 2,000+ collections across 30+ marketplaces
  • +
  • Enterprise-grade uptime — built for high-volume API usage
  • +
+
- -
- - - -
-
-

Explore Our API

-
- - <% coins.forEach(coin => { %> -
-
- <%= coin.name %> -
-
<%= coin.name %> (<%= coin.symbol.toUpperCase() %>)
-

$<%= coin.current_price?.toLocaleString() || 'N/A' %>

-

- - <%= coin.price_change_percentage_24h >= 0 ? '+' : '' %><%= coin.price_change_percentage_24h?.toFixed(2) || '0.00' %>% (24h) -

-

Market Cap: $<%= coin.market_cap?.toLocaleString() || 'N/A' %>

-

Volume (24h): $<%= coin.total_volume?.toLocaleString() || 'N/A' %>

-

Circulating Supply: <%= coin.circulating_supply?.toLocaleString() || 'N/A' %> <%= coin.symbol.toUpperCase() %>

+ + + +
+

What Our Users Say

+ <% + var testimonials = [ + { name: 'Alex M.', role: 'Day Trader', quote: 'CryptoPulse gives me the real-time edge I need. No delays, no excuses.' }, + { name: 'Sarah K.', role: 'DeFi Developer', quote: 'The API is rock solid. 200+ networks in one place is a game-changer.' }, + { name: 'James T.', role: 'Portfolio Manager', quote: 'Finally a dashboard that\'s clean and actually fast. Love the portfolio tracking.' }, + { name: 'Priya N.', role: 'NFT Collector', quote: 'Floor price tracking across 30+ marketplaces? Insane. Nothing else comes close.' }, + { name: 'Carlos R.', role: 'Quant Analyst', quote: '10 years of historical OHLCV data with minute granularity. Exactly what I needed.' }, + { name: 'Emma L.', role: 'Crypto Blogger', quote: 'I embed CryptoPulse data in every post. My readers trust the numbers.' }, + { name: 'David W.', role: 'Retail Investor', quote: 'Set up my watchlist in minutes. The UI is so much cleaner than the big sites.' }, + { name: 'Yuki H.', role: 'Blockchain Eng.', quote: 'Free tier is generous and the docs are actually good. Rare combination.' } + ]; + %> +
+
+ <% for (var r = 0; r < 2; r++) { testimonials.forEach(function(t) { %> +
+
+
<%= t.name.charAt(0) %>
+
+
<%= t.name %>
+
<%= t.role %>
+
★★★★★
+

"<%= t.quote %>"

- <% }) %> + <% }); } %>
- - - -
-
- - - - -

Explore our Graphs

- - -

- Market cap charts display the total value of a cryptocurrency, calculated by multiplying its price by the circulating supply. They help assess a coin's size and dominance in the market. -

- - - - View Graphs - - - -
+
+ <% for (var r2 = 0; r2 < 2; r2++) { testimonials.forEach(function(t) { %> +
+
+
<%= t.name.charAt(0) %>
+
+
<%= t.name %>
+
<%= t.role %>
+
+
+
★★★★★
+

"<%= t.quote %>"

+
+ <% }); } %>
-
- <%- include('partials/footer') %> - - - - - +
+ + +
+

Why Choose CryptoPulse?

+
+
+

Others

+
    +
  • Delayed or cached price data
  • +
  • Cluttered, hard-to-navigate UI
  • +
  • No built-in portfolio tracking
  • +
  • Key features behind paywalls
  • +
  • No watchlist functionality
  • +
+
+
+

CryptoPulse

+
    +
  • Real-time prices, updated live
  • +
  • Clean, intuitive dashboard
  • +
  • Full portfolio tracking built in
  • +
  • Free data access, no paywalls
  • +
  • Custom watchlists for any coin
  • +
+
+
+
+ +<%- include('partials/footer') %> + + + + + - \ No newline at end of file + diff --git a/views/partials/about.ejs b/views/partials/about.ejs index 710ec59..9bd56c4 100644 --- a/views/partials/about.ejs +++ b/views/partials/about.ejs @@ -1,117 +1,118 @@ - - - - - - About CryptoPulse - - - - - - - - - -<%- include('header') %> - -
- - -
-

About CryptoPulse

-

- Empowering investors with a clear, comprehensive, and real-time view of the cryptocurrency market to foster informed and confident decision-making. -

-
- - -
-
-

The Challenge

-

- Keeping up with crypto can be tough. The market is wild, and good information is scattered all over the place, making it hard to see the big picture and know what really matters. -

-
-
-
-
-

Our Solution

-

- That's why we built CryptoPulse. It brings all the important data from trusted sources into one simple dashboard. With our easy-to-use charts and portfolio tracker, we turn confusing numbers into clear insights you can actually use. -

-
- Cryptocurrency exchange logo featuring stylized arrows forming a circular motion, representing dynamic trading activity. The logo is set against a clean background, conveying a sense of modern technology and energetic movement. No visible text in the image. -
-
-
-
- - -
-

Key Features

-

- A suite of powerful tools designed for the modern investor. -

-
- -
-
-

Real-Time Market Data

-

Get live prices for thousands of coins from the best exchanges.

-
- -
-
-

Advanced Charting

-

Explore how coins have performed over time with our interactive charts.

-
- -
-
-

Portfolio Management

-

Keep an eye on all your investments and see how you're doing in one spot.

-
- -
-
-

Overall Performance

-

Understand your strategy and see how your whole portfolio is doing.

-
-
-
- - -
-
-

Technology & Architecture

-

Built with a modern, scalable, and robust tech stack to ensure performance and reliability.

-
-
-

Frontend

  • 🎨 Bootstrap CSS
  • Tailwind CSS
  • 📊 Charts
-

Backend

  • ⚙️ Node.js & Express
  • 🔌 WebSocket for Real-time
-

APIs

  • 🗄️ CoinGecko
  • 🚀 GitHub Actions for CI/CD
-
-
- - -
-
- Vedant Wagh -
-

Vedant Wagh

-

Full-Stack Developer | Music Production Enthusiast

-

I'm a passionate developer fascinated by decentralized tech and financial markets. CryptoPulse is where my skills in web development meet my excitement for the potential of cryptocurrency.

-
-
-
-
- -
- - -<%- include('footer') %> - - - - + + + + + + About CryptoPulse + + + + + + + + + + +<%- include('header') %> + +
+ + +
+

About CryptoPulse

+

+ Empowering investors with a clear, comprehensive, and real-time view of the cryptocurrency market to foster informed and confident decision-making. +

+
+ + +
+
+

The Challenge

+

+ Keeping up with crypto can be tough. The market is wild, and good information is scattered all over the place, making it hard to see the big picture and know what really matters. +

+
+
+
+
+

Our Solution

+

+ That's why we built CryptoPulse. It brings all the important data from trusted sources into one simple dashboard. With our easy-to-use charts and portfolio tracker, we turn confusing numbers into clear insights you can actually use. +

+
+ Cryptocurrency exchange logo featuring stylized arrows forming a circular motion, representing dynamic trading activity. The logo is set against a clean background, conveying a sense of modern technology and energetic movement. No visible text in the image. +
+
+
+
+ + +
+

Key Features

+

+ A suite of powerful tools designed for the modern investor. +

+
+ +
+
+

Real-Time Market Data

+

Get live prices for thousands of coins from the best exchanges.

+
+ +
+
+

Advanced Charting

+

Explore how coins have performed over time with our interactive charts.

+
+ +
+
+

Portfolio Management

+

Keep an eye on all your investments and see how you're doing in one spot.

+
+ +
+
+

Overall Performance

+

Understand your strategy and see how your whole portfolio is doing.

+
+
+
+ + +
+
+

Technology & Architecture

+

Built with a modern, scalable, and robust tech stack to ensure performance and reliability.

+
+
+

Frontend

  • 🎨 Bootstrap CSS
  • Tailwind CSS
  • 📊 Charts
+

Backend

  • ⚙️ Node.js & Express
  • 🔌 WebSocket for Real-time
+

APIs

  • 🗄️ CoinGecko
  • 🚀 GitHub Actions for CI/CD
+
+
+ + +
+
+ Vedant Wagh +
+

Vedant Wagh

+

Full-Stack Developer | Music Production Enthusiast

+

I'm a passionate developer fascinated by decentralized tech and financial markets. CryptoPulse is where my skills in web development meet my excitement for the potential of cryptocurrency.

+
+
+
+
+ +
+ + +<%- include('footer') %> + + + + diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs index 8ae1500..1a8734d 100644 --- a/views/partials/footer.ejs +++ b/views/partials/footer.ejs @@ -1,5 +1,72 @@ -
- -
+ diff --git a/views/partials/header.ejs b/views/partials/header.ejs index e56aab6..73552ca 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -1,107 +1,35 @@ -
- -
- - - Crypto - Pulse - - Cryptocurrency exchange logo - - -
-
- - + diff --git a/views/partials/markets.ejs b/views/partials/markets.ejs index 635d928..ccb61f1 100644 --- a/views/partials/markets.ejs +++ b/views/partials/markets.ejs @@ -1,34 +1,67 @@ - - - Market - - - + + + Markets — CryptoPulse + + + + - <%- include('header') %> -

Market Data

- + <%- include('header') %> + +
+ +
+

Markets

+ +
+ + + +
+
+
+ +
+
- - - - - - - + + + + + + + + - + -
#NameCurrent PriceMarket CapTotal Volume
#CoinPrice24h %Market CapVolume (24h)
-
-

Loading market data...

+
- <%- include('footer') %> - + +
+ + <%- include('footer') %> + + + + + - \ No newline at end of file + diff --git a/views/portfolio/dashboard.ejs b/views/portfolio/dashboard.ejs index 20765db..843c039 100644 --- a/views/portfolio/dashboard.ejs +++ b/views/portfolio/dashboard.ejs @@ -1,465 +1,472 @@ - - - - - - Portfolio - CryptoPulse - - - - - - - <%- include('../partials/header') %> - -
- -
-
-

My Portfolio

-

Track and manage your cryptocurrency investments

-
- -
- - -
-
-
Total Value
-
$<%= totalValue.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
-
-
-
Total Invested
-
$<%= totalInvested.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
-
-
-
Profit / Loss
-
- <%= totalProfitLoss >= 0 ? '+' : '' %>$<%= totalProfitLoss.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %> - (<%= totalProfitLossPercent >= 0 ? '+' : '' %><%= totalProfitLossPercent.toFixed(2) %>%) -
-
-
- - -
-

My Holdings

- <% if (holdings.length === 0) { %> -
- - - - - -

No Holdings Yet

-

Start building your portfolio by adding your first cryptocurrency holding.

- -
- <% } else { %> -
- - - - - - - - - - - - - - <% holdings.forEach(holding => { %> - - - - - - - - - - <% }) %> - -
CoinAmountAvg. PriceCurrent PriceValueP/LActions
-
- <%= holding.name %> -
-
<%= holding.name %>
-
<%= holding.symbol %>
-
-
-
<%= holding.quantity.toLocaleString('en-US', {maximumFractionDigits: 8}) %>$<%= holding.purchasePrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %> -
$<%= holding.currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
-
- <%= holding.priceChange24h >= 0 ? '+' : '' %><%= holding.priceChange24h.toFixed(2) %>% -
-
$<%= holding.currentValue.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %> - <%= holding.profitLossPercent >= 0 ? '+' : '' %><%= holding.profitLossPercent.toFixed(2) %>% - -
- - -
-
-
- <% } %> -
- - <% if (holdings.length > 0) { %> - -
-

Portfolio Analytics

- -
- -
-

Allocation by Coin

-
- -
-
- - -
-

Portfolio Performance

-
- - - - -
-
- -
-
-
- - -
-
-
Best Performer
- <% const bestPerformer = holdings.reduce((best, h) => h.profitLossPercent > best.profitLossPercent ? h : best, holdings[0]); %> -
- <%= bestPerformer.name %> -
-
<%= bestPerformer.name %> (<%= bestPerformer.symbol %>)
-
+<%= bestPerformer.profitLossPercent.toFixed(2) %>%
-
-
-
-
-
Worst Performer
- <% const worstPerformer = holdings.reduce((worst, h) => h.profitLossPercent < worst.profitLossPercent ? h : worst, holdings[0]); %> -
- <%= worstPerformer.name %> -
-
<%= worstPerformer.name %> (<%= worstPerformer.symbol %>)
-
- <%= worstPerformer.profitLossPercent >= 0 ? '+' : '' %><%= worstPerformer.profitLossPercent.toFixed(2) %>% -
-
-
-
-
- - -
-

Performance Metrics

-
-
-
24H Change
- <% const avg24hChange = holdings.reduce((sum, h) => sum + (h.priceChange24h * h.currentValue), 0) / totalValue; %> -
- <%= avg24hChange >= 0 ? '+' : '' %><%= avg24hChange.toFixed(2) %>% -
-
-
-
Total Return
-
- <%= totalProfitLossPercent >= 0 ? '+' : '' %><%= totalProfitLossPercent.toFixed(2) %>% -
-
-
-
Total Coins
-
<%= holdings.length %>
-
-
-
Avg. Position Size
-
$<%= (totalValue / holdings.length).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
-
-
-
-
- <% } %> -
- - - - - - - - <%- include('../partials/footer') %> - - - - - <% if (holdings.length > 0) { %> - - <% } %> - - + + + + + + Portfolio - CryptoPulse + + + + + + + + <%- include('../partials/header') %> + +
+ +
+
+

My Portfolio

+

Track and manage your cryptocurrency investments

+
+ +
+ + +
+
+
Total Value
+
$<%= totalValue.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
+
+
+
Total Invested
+
$<%= totalInvested.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
+
+
+
Profit / Loss
+
+ <%= totalProfitLoss >= 0 ? '+' : '' %>$<%= totalProfitLoss.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %> + (<%= totalProfitLossPercent >= 0 ? '+' : '' %><%= totalProfitLossPercent.toFixed(2) %>%) +
+
+
+ + +
+

My Holdings

+ <% if (holdings.length === 0) { %> +
+ + + + + +

No Holdings Yet

+

Start building your portfolio by adding your first cryptocurrency holding.

+ +
+ <% } else { %> +
+ + + + + + + + + + + + + + <% holdings.forEach(holding => { %> + + + + + + + + + + <% }) %> + +
CoinAmountAvg. PriceCurrent PriceValueP/LActions
+
+ <%= holding.name %> +
+
<%= holding.name %>
+
<%= holding.symbol %>
+
+
+
<%= holding.quantity.toLocaleString('en-US', {maximumFractionDigits: 8}) %>$<%= holding.purchasePrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %> +
$<%= holding.currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
+
+ <%= holding.priceChange24h >= 0 ? '+' : '' %><%= holding.priceChange24h.toFixed(2) %>% +
+
$<%= holding.currentValue.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %> + <%= holding.profitLossPercent >= 0 ? '+' : '' %><%= holding.profitLossPercent.toFixed(2) %>% + +
+ + +
+
+
+ <% } %> +
+ + <% if (holdings.length > 0) { %> + +
+

Portfolio Analytics

+ +
+ +
+

Allocation by Coin

+
+ +
+
+ + +
+

Portfolio Performance

+
+ + + + +
+
+ +
+
+
+ + +
+
+
Best Performer
+ <% const bestPerformer = holdings.reduce((best, h) => h.profitLossPercent > best.profitLossPercent ? h : best, holdings[0]); %> +
+ <%= bestPerformer.name %> +
+
<%= bestPerformer.name %> (<%= bestPerformer.symbol %>)
+
+<%= bestPerformer.profitLossPercent.toFixed(2) %>%
+
+
+
+
+
Worst Performer
+ <% const worstPerformer = holdings.reduce((worst, h) => h.profitLossPercent < worst.profitLossPercent ? h : worst, holdings[0]); %> +
+ <%= worstPerformer.name %> +
+
<%= worstPerformer.name %> (<%= worstPerformer.symbol %>)
+
+ <%= worstPerformer.profitLossPercent >= 0 ? '+' : '' %><%= worstPerformer.profitLossPercent.toFixed(2) %>% +
+
+
+
+
+ + +
+

Performance Metrics

+
+
+
24H Change
+ <% const avg24hChange = holdings.reduce((sum, h) => sum + (h.priceChange24h * h.currentValue), 0) / totalValue; %> +
+ <%= avg24hChange >= 0 ? '+' : '' %><%= avg24hChange.toFixed(2) %>% +
+
+
+
Total Return
+
+ <%= totalProfitLossPercent >= 0 ? '+' : '' %><%= totalProfitLossPercent.toFixed(2) %>% +
+
+
+
Total Coins
+
<%= holdings.length %>
+
+
+
Avg. Position Size
+
$<%= (totalValue / holdings.length).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) %>
+
+
+
+
+ <% } %> +
+ + + + + + + + <%- include('../partials/footer') %> + + + + + <% if (holdings.length > 0) { %> + + <% } %> + + + diff --git a/views/portfolio/watchlist.ejs b/views/portfolio/watchlist.ejs index c23a1d3..2f1e01f 100644 --- a/views/portfolio/watchlist.ejs +++ b/views/portfolio/watchlist.ejs @@ -1,263 +1,265 @@ - - - - - - Watchlist - CryptoPulse - - - - - - - <%- include('../partials/header') %> - -
- -
-
-

My Watchlist

-

Track cryptocurrencies you're interested in

-
- -
- - -
- <% if (watchlist.length === 0) { %> -
- - - -

Your Watchlist is Empty

-

Start tracking cryptocurrencies by adding them to your watchlist.

- -
- <% } else { %> - -
- - - - - - - - - - - - - - - <% watchlist.forEach((coin, index) => { %> - - - - - - - - - - - <% }) %> - -
#CoinPrice24h %7d %Market CapVolume (24h)Actions
<%= index + 1 %> -
- <%= coin.name %> -
-
<%= coin.name %>
-
<%= coin.symbol %>
-
-
-
$<%= coin.currentPrice ? coin.currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: coin.currentPrice < 1 ? 6 : 2}) : '--' %> - - <%= coin.priceChange24h >= 0 ? '+' : '' %><%= coin.priceChange24h ? coin.priceChange24h.toFixed(2) : '0.00' %>% - - - - <%= coin.priceChange7d >= 0 ? '+' : '' %><%= coin.priceChange7d ? coin.priceChange7d.toFixed(2) : '0.00' %>% - - $<%= coin.marketCap ? (coin.marketCap / 1e9).toFixed(2) + 'B' : '--' %>$<%= coin.volume24h ? (coin.volume24h / 1e9).toFixed(2) + 'B' : '--' %> -
- - - -
-
-
- - -
-
- - Set Alert -
-
- - Add to Portfolio -
-
- - Remove -
-
- <% } %> -
-
- - - - - - - - - - - - - - <%- include('../partials/footer') %> - - - - - + + + + + + Watchlist - CryptoPulse + + + + + + + + <%- include('../partials/header') %> + +
+ +
+
+

My Watchlist

+

Track cryptocurrencies you're interested in

+
+ +
+ + +
+ <% if (watchlist.length === 0) { %> +
+ + + +

Your Watchlist is Empty

+

Start tracking cryptocurrencies by adding them to your watchlist.

+ +
+ <% } else { %> + +
+ + + + + + + + + + + + + + + <% watchlist.forEach((coin, index) => { %> + + + + + + + + + + + <% }) %> + +
#CoinPrice24h %7d %Market CapVolume (24h)Actions
<%= index + 1 %> +
+ <%= coin.name %> +
+
<%= coin.name %>
+
<%= coin.symbol %>
+
+
+
$<%= coin.currentPrice ? coin.currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: coin.currentPrice < 1 ? 6 : 2}) : '--' %> + + <%= coin.priceChange24h >= 0 ? '+' : '' %><%= coin.priceChange24h ? coin.priceChange24h.toFixed(2) : '0.00' %>% + + + + <%= coin.priceChange7d >= 0 ? '+' : '' %><%= coin.priceChange7d ? coin.priceChange7d.toFixed(2) : '0.00' %>% + + $<%= coin.marketCap ? (coin.marketCap / 1e9).toFixed(2) + 'B' : '--' %>$<%= coin.volume24h ? (coin.volume24h / 1e9).toFixed(2) + 'B' : '--' %> +
+ + + +
+
+
+ + +
+
+ + Set Alert +
+
+ + Add to Portfolio +
+
+ + Remove +
+
+ <% } %> +
+
+ + + + + + + + + + + + + + <%- include('../partials/footer') %> + + + + + +