From 4971ddceafa3b023cc94968afa98f77ecc1ad149 Mon Sep 17 00:00:00 2001 From: Kansaram Date: Mon, 8 Jun 2026 12:44:38 +0530 Subject: [PATCH] feat: animated forex cards background on login page Canvas animation with 14 floating forex pair cards (price + sparkline chart), 30 floating data particles, grid overlay and center glow radial gradient. Cards drift upward with sine-wave wobble. Login card has backdrop-blur overlay. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/LoginPage.jsx | 340 ++++++++++++++++++++++--------- 1 file changed, 244 insertions(+), 96 deletions(-) diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index eb74a5c..0124fec 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -1,9 +1,200 @@ -import { useState } from 'react' +import { useState, useEffect, useRef } from 'react' import { NexusIcon } from '../components/NexusLogo' const USER = 'mihir.kansara' const PASS = 'nexus@2026' +const PAIRS = [ + { pair:'EUR/USD', price:'1.08542', chg:'+0.23%', up:true, spark:[40,38,42,39,44,41,46,43,45,48] }, + { pair:'GBP/USD', price:'1.26381', chg:'-0.14%', up:false, spark:[55,52,54,50,53,49,51,48,50,47] }, + { pair:'USD/JPY', price:'157.234', chg:'+0.41%', up:true, spark:[30,33,31,35,34,37,36,39,38,42] }, + { pair:'XAU/USD', price:'2341.50', chg:'+0.67%', up:true, spark:[20,24,22,28,25,30,27,32,29,35] }, + { pair:'USD/CHF', price:'0.90124', chg:'-0.08%', up:false, spark:[60,58,59,56,57,54,56,53,54,51] }, + { pair:'AUD/USD', price:'0.65432', chg:'+0.19%', up:true, spark:[25,27,26,29,28,31,30,33,32,35] }, + { pair:'EUR/JPY', price:'170.341', chg:'+0.55%', up:true, spark:[15,18,16,20,19,22,21,24,23,27] }, + { pair:'GBP/JPY', price:'198.712', chg:'-0.22%', up:false, spark:[70,67,68,64,65,61,62,58,59,55] }, + { pair:'NZD/USD', price:'0.61234', chg:'+0.11%', up:true, spark:[35,37,36,39,38,41,40,43,42,45] }, + { pair:'USD/CAD', price:'1.36542', chg:'-0.17%', up:false, spark:[50,48,49,46,47,44,45,42,43,40] }, + { pair:'EUR/GBP', price:'0.85621', chg:'+0.09%', up:true, spark:[28,30,29,32,31,34,33,36,35,38] }, + { pair:'BTC/USD', price:'67,420', chg:'+1.23%', up:true, spark:[10,15,12,18,14,20,16,22,18,25] }, +] + +function FloatingBg() { + const canvasRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + const ctx = canvas.getContext('2d') + let raf + + const resize = () => { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + } + resize() + window.addEventListener('resize', resize) + + // Create floating card objects + const cards = Array.from({ length: 14 }, (_, i) => { + const p = PAIRS[i % PAIRS.length] + return { + ...p, + x: Math.random() * window.innerWidth, + y: Math.random() * window.innerHeight, + vx: (Math.random() - 0.5) * 0.35, + vy: -(Math.random() * 0.4 + 0.15), + opacity: Math.random() * 0.28 + 0.08, + scale: Math.random() * 0.35 + 0.65, + rot: (Math.random() - 0.5) * 0.08, + phase: Math.random() * Math.PI * 2, + } + }) + + // Floating data particles + const particles = Array.from({ length: 30 }, () => ({ + x: Math.random() * window.innerWidth, + y: Math.random() * window.innerHeight, + vy: -(Math.random() * 0.5 + 0.2), + text: ['▲ 0.' + Math.floor(Math.random()*99).toString().padStart(2,'0') + '%', + '▼ 0.' + Math.floor(Math.random()*99).toString().padStart(2,'0') + '%', + (Math.random() > 0.5 ? '+' : '-') + (Math.random()*2).toFixed(4), + Math.floor(Math.random()*9999).toString()][Math.floor(Math.random()*4)], + color: Math.random() > 0.5 ? '#00c8f0' : Math.random() > 0.5 ? '#3ddc84' : '#ef4444', + opacity: Math.random() * 0.18 + 0.05, + size: Math.random() * 4 + 8, + })) + + let t = 0 + + function drawSparkline(ctx, spark, x, y, w, h, color) { + const min = Math.min(...spark), max = Math.max(...spark) + const range = max - min || 1 + ctx.beginPath() + spark.forEach((v, i) => { + const px = x + (i / (spark.length - 1)) * w + const py = y + h - ((v - min) / range) * h + i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py) + }) + ctx.strokeStyle = color + ctx.lineWidth = 1.2 + ctx.stroke() + // Last dot + const lx = x + w, ly = y + h - ((spark[spark.length-1] - min) / range) * h + ctx.beginPath() + ctx.arc(lx, ly, 2, 0, Math.PI * 2) + ctx.fillStyle = color + ctx.fill() + } + + function drawCard(ctx, card, t) { + const W = 148, H = 68 + const cx = card.x, cy = card.y + Math.sin(t * 0.0008 + card.phase) * 6 + + ctx.save() + ctx.globalAlpha = card.opacity + ctx.translate(cx, cy) + ctx.scale(card.scale, card.scale) + ctx.rotate(card.rot + Math.sin(t * 0.0004 + card.phase) * 0.015) + + // Card bg + ctx.fillStyle = 'rgba(2,8,23,0.72)' + ctx.strokeStyle = card.up ? 'rgba(0,200,240,0.25)' : 'rgba(239,68,68,0.2)' + ctx.lineWidth = 1 + ctx.beginPath() + ctx.roundRect(-W/2, -H/2, W, H, 6) + ctx.fill() + ctx.stroke() + + // Pair label + ctx.fillStyle = '#e2e8f0' + ctx.font = `600 11px "JetBrains Mono", monospace` + ctx.fillText(card.pair, -W/2 + 10, -H/2 + 16) + + // Price + ctx.fillStyle = '#00c8f0' + ctx.font = `700 13px "JetBrains Mono", monospace` + ctx.fillText(card.price, -W/2 + 10, -H/2 + 32) + + // Change + ctx.fillStyle = card.up ? '#3ddc84' : '#ef4444' + ctx.font = `600 9px "JetBrains Mono", monospace` + ctx.fillText((card.up ? '▲ ' : '▼ ') + card.chg, -W/2 + 10, -H/2 + 46) + + // Sparkline + drawSparkline(ctx, card.spark, W/2 - 55, -H/2 + 12, 44, 36, + card.up ? 'rgba(61,220,132,0.7)' : 'rgba(239,68,68,0.7)') + + ctx.restore() + } + + function draw() { + t++ + const W = canvas.width, H = canvas.height + ctx.clearRect(0, 0, W, H) + + // Deep dark bg + ctx.fillStyle = '#020817' + ctx.fillRect(0, 0, W, H) + + // Grid + ctx.strokeStyle = 'rgba(0,200,240,0.025)' + ctx.lineWidth = 1 + for (let x = 0; x < W; x += 44) { + ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke() + } + for (let y = 0; y < H; y += 44) { + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke() + } + + // Center glow + const g = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, W * 0.45) + g.addColorStop(0, 'rgba(0,200,240,0.06)') + g.addColorStop(1, 'rgba(0,0,0,0)') + ctx.fillStyle = g + ctx.fillRect(0, 0, W, H) + + // Particles + particles.forEach(p => { + p.y += p.vy + if (p.y < -20) { p.y = H + 10; p.x = Math.random() * W } + ctx.globalAlpha = p.opacity + ctx.fillStyle = p.color + ctx.font = `${p.size}px "JetBrains Mono", monospace` + ctx.fillText(p.text, p.x, p.y) + }) + ctx.globalAlpha = 1 + + // Cards + cards.forEach(card => { + card.x += card.vx + card.y += card.vy + if (card.y < -100) { + card.y = H + 80 + card.x = Math.random() * W + } + if (card.x < -180) card.x = W + 80 + if (card.x > W + 180) card.x = -80 + drawCard(ctx, card, t) + }) + + raf = requestAnimationFrame(draw) + } + + draw() + return () => { + cancelAnimationFrame(raf) + window.removeEventListener('resize', resize) + } + }, []) + + return ( + + ) +} + export default function LoginPage({ onLogin }) { const [u, setU] = useState('') const [p, setP] = useState('') @@ -36,8 +227,8 @@ export default function LoginPage({ onLogin }) { }}> - {/* Scanline effect */} -
+ - {/* Grid bg */} + {/* Blur overlay behind card */}
- - {/* Glow orb */} -
{/* Login card */}
{/* Logo */} -
- -
+
+ +
NEXUS
-
+
TERMINAL · RESTRICTED ACCESS
- {/* Divider */} -
+
- {/* Form */} -
+
-
- USER ID -
- { setU(e.target.value); setErr('') }} - autoComplete="off" - spellCheck={false} - /> +
USER ID
+ { setU(e.target.value); setErr('') }} + autoComplete="off" spellCheck={false}/>
-
- ACCESS KEY -
- { setP(e.target.value); setErr('') }} - /> +
ACCESS KEY
+ { setP(e.target.value); setErr('') }}/>
{err && (
- ⚠ {err} -
+ fontSize:11, color:'var(--red)', textAlign:'center', + background:'rgba(239,68,68,0.08)', border:'1px solid rgba(239,68,68,0.2)', + borderRadius:4, padding:'8px 12px', letterSpacing:'0.05em', + }}>⚠ {err}
)} - - {/* DM section */} -